diff --git a/docs/rfc/implemented/feature/2026-06-15-code-mode.md b/docs/rfc/implemented/feature/2026-06-15-code-mode.md index 2f643b432e..5ce9d784b9 100644 --- a/docs/rfc/implemented/feature/2026-06-15-code-mode.md +++ b/docs/rfc/implemented/feature/2026-06-15-code-mode.md @@ -90,7 +90,7 @@ What exists now: - **The seam**: `packages/code-runtime/` — `@deepseek-ai/dsh-code-runtime` (abstract `CodeRuntime`, the vocabulary above, `ctx.codeRuntime`) and `@deepseek-ai/dsh-code-runtime-worker` (the worker-thread backend, every cap a validated config field). Rows in the service map, capability-seams graph, config catalog, and cordis catalog. - **The registry surface**: `ToolRegistry`'s first config (`mode`), the mode-aware wire contribution, the `tools:sdk` section, `jsonSchemaToTs`/`renderToolsSdk` (exported), `run_code` + the dispatch bridge + `CodeRunFailedError`, and the `tool/code-dispatch` log event (declaration-merged into `SessionEventMap`, regenerated into the persistence catalog; `run_code` in the tool catalog). -- **The composed surface**: the `tools` config forwards through `agent-core` and both app packages (`stdio-agent`, `acp-agent`); `examples/code-agent` + `demo:code` run the worker runtime under `mode: 'code'`; the adding-a-tool cookbook states that a registered tool is reachable from programs for free, and the tool-pipeline doc shows sub-dispatches re-entering both waterfalls. +- **The composed surface**: the `tools` config forwards through `agent-core` and both app packages (`stdio-agent`, `acp-agent`); `examples/code-agent` + `demo:code-mode` run the worker runtime under `mode: 'code'`; the adding-a-tool cookbook states that a registered tool is reachable from programs for free, and the tool-pipeline doc shows sub-dispatches re-entering both waterfalls. - **Interactions inherited by deployments**: a `toolOrder` naming native tools rejects every assembly under `'code'` (update or drop the order config when switching modes); sub-call `additionalContext` is dropped by the bridge (a plural context channel is deferred until a real hook needs it through Code Mode); sub-dispatch stays serialized until tools can declare concurrency safety — the same metadata the native parallel-dispatch TODO waits on. ## Testing diff --git a/examples/README.md b/examples/README.md index d42144ec9a..3027d01f0a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -23,10 +23,10 @@ Run with: `pnpm run demo:repl` (needs `DEEPSEEK_API_KEY` in the environment or a The coding agent flipped to **Code Mode**: the worker-thread code runtime is loaded and the tool registry runs `mode: code`, so the model gets exactly one wire tool — `run_code` — plus a generated TypeScript SDK section, and composes bash/read/write/edit/todo_write by writing a program whose output it curates. -Run with: `pnpm run demo:code` (needs `DEEPSEEK_API_KEY`). See [code-agent/README.md](code-agent/README.md) for what to try and how it differs from coding-agent. +Run with: `pnpm run demo:code-mode` (needs `DEEPSEEK_API_KEY`; the REPL is the default UI — `acp` as the argument serves the acp-agent example's Code Mode overlay instead). See [code-agent/README.md](code-agent/README.md) for what to try and how it differs from coding-agent. ## 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. -Run with: `pnpm run demo:acp` (needs `DEEPSEEK_API_KEY`); `pnpm run demo:acp-code` 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. +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. diff --git a/examples/acp-agent/README.md b/examples/acp-agent/README.md index b00604a056..5b3c936651 100644 --- a/examples/acp-agent/README.md +++ b/examples/acp-agent/README.md @@ -4,10 +4,10 @@ The DeepSeek Harness SDK agent demo exposed as an **Agent Client Protocol (ACP)* ```sh pnpm run demo:acp # needs DEEPSEEK_API_KEY (repo-root .env or env) -pnpm run demo:acp-code # the same server in Code Mode: one wire tool, run_code +pnpm run demo:code-mode acp # the same server in Code Mode: one wire tool, run_code ``` -This example is just a leaf `cordis.yml`: it loads the [`@deepseek-ai/dsh-acp-agent`](../../packages/ui/acp-agent) app (which bundles the [`@deepseek-ai/dsh-agent-core`](../../packages/core/agent-core) spine, JSONL session persistence, and the `@deepseek-ai/dsh-acp` bridge — with **no pre-created agents**, since ACP `session/new` creates them on demand), the swappable DeepSeek, bash, and filesystem backends, the model-facing `read`/`write`/`edit`/`subagent`/`subagent_fork`/`todo_write` tool entries, and the advisory `repeat-tool-guard` loop-hygiene plugin. The app package bakes in the no-stdout-logger cluster, so a leaf has no logger entry to get wrong by default — keeping stdout pure for JSON-RPC. `demo:acp-code` boots the same tree through the [`code-mode.cordis.yml`](code-mode.cordis.yml) overlay — the tool surface collapses to `run_code` + the generated TypeScript SDK, dispatching through the worker-thread code runtime (see the [dsh-tools Code Mode section](../../packages/core/tools/README.md#code-mode)). +This example is just a leaf `cordis.yml`: it loads the [`@deepseek-ai/dsh-acp-agent`](../../packages/ui/acp-agent) app (which bundles the [`@deepseek-ai/dsh-agent-core`](../../packages/core/agent-core) spine, JSONL session persistence, and the `@deepseek-ai/dsh-acp` bridge — with **no pre-created agents**, since ACP `session/new` creates them on demand), the swappable DeepSeek, bash, and filesystem backends, the model-facing `read`/`write`/`edit`/`subagent`/`subagent_fork`/`todo_write` tool entries, and the advisory `repeat-tool-guard` loop-hygiene plugin. The app package bakes in the no-stdout-logger cluster, so a leaf has no logger entry to get wrong by default — keeping stdout pure for JSON-RPC. `demo:code-mode acp` boots the same tree through the [`code-mode.cordis.yml`](code-mode.cordis.yml) overlay — the tool surface collapses to `run_code` + the generated TypeScript SDK, dispatching through the worker-thread code runtime (see the [dsh-tools Code Mode section](../../packages/core/tools/README.md#code-mode)). ## stdout is the protocol diff --git a/examples/acp-agent/code-mode.cordis.yml b/examples/acp-agent/code-mode.cordis.yml index 0dfbd73d26..d46e490494 100644 --- a/examples/acp-agent/code-mode.cordis.yml +++ b/examples/acp-agent/code-mode.cordis.yml @@ -3,7 +3,7 @@ # (the registry offers exactly one wire tool, run_code, plus the generated # TypeScript SDK prompt section) and the worker-thread code runtime joins the # tree as `ctx.codeRuntime`. The dsh-acp-agent bin boots this file for -# `pnpm run demo:acp-code` and when the snapshot harness records the +# `pnpm run demo:code-mode acp` and when the snapshot harness records the # code-mode scenarios; DSH_SNAPSHOT=replay swaps it for the sibling # code-mode.cordis.snapshot.yml. A config patch REPLACES the entry's whole # config, so the base entry's fields are restated verbatim. diff --git a/examples/code-agent/README.md b/examples/code-agent/README.md index 9b6619c0b3..6e08a883a3 100644 --- a/examples/code-agent/README.md +++ b/examples/code-agent/README.md @@ -3,7 +3,7 @@ The [Code Mode](../../docs/rfc/implemented/feature/2026-06-15-code-mode.md) form of the coding agent: instead of one native tool call per step, the model is offered exactly ONE wire tool — `run_code` — plus a generated TypeScript SDK section declaring every other registered tool (`bash`, `read`, `write`, `edit`, `todo_write`). The model composes tools by writing a program; the program runs in a fresh worker thread (`@deepseek-ai/dsh-code-runtime-worker`), its tool calls bridge back through the ordinary `tools/pre-execute`/`post-execute` pipeline one at a time, each is logged as a `tool/code-dispatch` session event, and ONLY what the program prints or returns re-enters the model's context. ```sh -pnpm run demo:code # needs DEEPSEEK_API_KEY (repo-root .env works) +pnpm run demo:code-mode # needs DEEPSEEK_API_KEY (repo-root .env works) ``` Try a task that spans several tool calls, e.g.: diff --git a/examples/code-agent/tests/keyless-smoke.e2e.ts b/examples/code-agent/tests/keyless-smoke.e2e.ts index 4b7a150e99..d5913e48d5 100644 --- a/examples/code-agent/tests/keyless-smoke.e2e.ts +++ b/examples/code-agent/tests/keyless-smoke.e2e.ts @@ -42,7 +42,7 @@ async function bootAndEof(): Promise<{ stdout: string; code: number }> { return new Promise((resolve, reject) => { const proc = spawn( process.execPath, - // --expose-internals: cordis.yml loads the HMR plugin (mirrors demo:code). + // --expose-internals: cordis.yml loads the HMR plugin (mirrors demo:code-mode). ['--expose-internals', '--import', tsxLoader, binScript, configPath], { cwd, diff --git a/package.json b/package.json index 4ed429f1d0..b906964b1e 100644 --- a/package.json +++ b/package.json @@ -63,9 +63,8 @@ "hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types", "demo:echo": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml", "demo:repl": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml", - "demo:code": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/code-agent/cordis.yml", + "demo:code-mode": "node scripts/demo-code-mode.mjs", "demo:acp": "node --import tsx packages/ui/acp-agent/src/bin.ts examples/acp-agent/cordis.yml", - "demo:acp-code": "node --import tsx packages/ui/acp-agent/src/bin.ts examples/acp-agent/code-mode.cordis.yml", "postinstall": "node scripts/install-lefthook.mjs" }, "devDependencies": { diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 7525fb8fd6..c0c7cfe437 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -133,7 +133,7 @@ Under `mode: code` (or `both`) the registry turns the tool surface into a progra - **The dispatch bridge** (`run_code`'s execute): every binding call is JSON-normalized BEFORE dispatch (a value that does not survive — `BigInt`, circulars — rejects that one call, so the dispatched form and the logged form are the same JSON value by construction), serialized through a per-run queue (even `Promise.all` executes the underlying `ctx.tools.execute()` calls one at a time in submission order — the tool contract carries no concurrency-safety metadata yet), gated by `tools/pre-execute`/`tools/post-execute` like any native call (a deny reaches the program as a binding rejection), and logged as one `tool/code-dispatch` session event (log-only: `deriveMessages()` never surfaces it) with the deterministic sub-id `:code:`. A failed sub-call REJECTS the program-side promise with the tool's error text — real code error handling, no bespoke envelope. A sub-call's `additionalContext` is deliberately DROPPED (no safe outlet mid-run without breaking tool-call/result adjacency; deferred until a real hook needs it through Code Mode). - **Settlement discipline**: the bridge owns a run-scoped abort that follows the outer signal in and fires when the run settles for any reason, so a budget expiry aborts an in-flight sub-tool instead of orphaning it; the bridge then drains its queue BEFORE returning, so every `tool/code-dispatch` lands inside the open turn. A failed run throws `CodeRunFailedError` (`code: 'CODE_RUN_FAILED'`, message = the failure kind + captured logs), which the pipeline converts to a structured `isError` the model self-corrects from. -The wire collapse is the registry's own contribution (`systemPrompt.tools()` is mode-aware), so the logged `request/header` records it for free — under `code`, the assembled tool list is exactly `[run_code]`, pinned by tests and the snapshot goldens. Try it: `pnpm run demo:code` ([examples/code-agent](../../../examples/code-agent/README.md)). +The wire collapse is the registry's own contribution (`systemPrompt.tools()` is mode-aware), so the logged `request/header` records it for free — under `code`, the assembled tool list is exactly `[run_code]`, pinned by tests and the snapshot goldens. Try it: `pnpm run demo:code-mode` ([examples/code-agent](../../../examples/code-agent/README.md)); `pnpm run demo:code-mode acp` serves the same mode over ACP instead of the REPL. ### What is NOT here (TODO) diff --git a/scripts/demo-code-mode.mjs b/scripts/demo-code-mode.mjs new file mode 100644 index 0000000000..465e5f2982 --- /dev/null +++ b/scripts/demo-code-mode.mjs @@ -0,0 +1,27 @@ +/** + * Boot the Code Mode demo under the UI named on the command line: + * `pnpm run demo:code-mode [repl|acp]`, default `repl`. Code Mode is the + * point — the UI is just the surface it happens to wear: `repl` starts the + * stdio REPL over examples/code-agent, `acp` starts the ACP server over + * examples/acp-agent's code-mode overlay. Both need DEEPSEEK_API_KEY + * (repo-root .env works). Anything else on the command line is a + * misconfiguration and fails loud with usage. + */ +import { spawn } from 'node:child_process' + +// Each UI's node invocation, verbatim what its standalone demo script ran +// (the stdio bin keeps --expose-internals for the cordis Loader's HMR path). +const UIS = new Map([ + ['repl', ['--expose-internals', '--import', 'tsx', 'packages/ui/stdio-agent/src/bin.ts', 'examples/code-agent/cordis.yml']], + ['acp', ['--import', 'tsx', 'packages/ui/acp-agent/src/bin.ts', 'examples/acp-agent/code-mode.cordis.yml']], +]) + +const ui = process.argv[2] ?? 'repl' +const args = UIS.get(ui) +if (!args || process.argv.length > 3) { + console.error('usage: pnpm run demo:code-mode [repl|acp]') + process.exit(2) +} + +const child = spawn(process.execPath, args, { stdio: 'inherit' }) +child.on('exit', (code, signal) => { process.exit(signal !== null ? 1 : code ?? 1) })