Files
deepseek-harness/packages/ui/acp-agent
Tianyi Cui b59d245c7c feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.

run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).

The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
..

@deepseek-ai/dsh-acp-agent

The ACP server app: a Cordis app plugin that composes the providerless agent spine (@deepseek-ai/dsh-agent-core) with the front-door cluster an Agent Client Protocol server needs, and a bin that boots a leaf cordis.yml speaking ACP JSON-RPC on stdio.

It is the structured counterpart to @deepseek-ai/dsh-stdio-agent: both consume the same spine, but this one bakes in the OPPOSITE front-door cluster.

What it bakes in — and what it deliberately omits

stdout is the ACP JSON-RPC channel, so the cluster is defined as much by what it LEAVES OUT as what it includes:

Plugin Why
@deepseek-ai/dsh-agent-core the spine, pre-creating no agents (ACP session/new creates them on demand)
@deepseek-ai/dsh-session-persistence-jsonl durable JSONL session log (the bridge advertises loadSession)
@deepseek-ai/dsh-acp the bridge that owns stdout for JSON-RPC
console logger omitted — it writes to stdout and would corrupt the protocol frames (the stdout-purity footgun)
hmr omitted — the editor owns the subprocess

Because the package wires no logger entry, an ACP leaf has nothing to get wrong by default: it only picks backends, so the common mistake — copying a console-logger entry from the stdio config — has no place here. (A leaf author technically can still add @cordisjs/plugin-logger-console as a sibling entry; the package can't forbid that. So the rule stands: never add a stdout logger to an ACP leaf — stdout is the JSON-RPC channel. Use a stderr exporter if you need logs.)

Config

Key Default Routed to
model (required) the per-session agent template the bridge creates agents from
persona the deployment persona template (may reference {{model}}/{{cwd}}), routed to dsh-system-prompt
toolOrder explicit model-facing tool order (a name list with one '<unlisted-tools>' rest entry; absent — lexicographic; an unregistered name fails each turn at prompt assembly), routed to dsh-system-prompt
persistenceRoot ./.sessions the JSONL backend's root directory

The leaf supplies the swappable backends: an LLM adapter (llm-deepseek for the real model, llm-replay for keyless snapshot replay) and a bash executor (bash-local).

The bin

dsh-acp-agent [path-to-cordis.yml] (default ./cordis.yml):

  • loads a gitignored .env from the cwd — skipped in snapshot REPLAY so a stray key can never trigger a live call;
  • honors DSH_SNAPSHOT=replay by booting the sibling cordis.snapshot.yml (the keyless replay tree, llm-replay in place of llm-deepseek);
  • in a snapshot run, disposes the context on stdin EOF so the session log is fully flushed before exit.

Run it under node --expose-internals: the cordis Loader resolves the config's bare plugin specifiers through its internal module loader, active only under that flag. (demo:acp runs under tsx, whose tsconfig paths map resolves them instead.)

All diagnostics go to stderr — stdout is the protocol.