dsh-agent
Interactive ACP v2 bridge for DeepSeek Harness (dsh) — streaming, run-state reporting, permissions. ALwith Desktop is its reference client.
- Stars
- 1
- Language
- TypeScript
- Created
- Aug 16, 2026
- Updated
- Oct 21, 2026
Introduction
dsh-agent
English | 中文
An interactive ACP v2 bridge for DeepSeek Harness (dsh): exposes a dsh agent as a chat backend that desktop/editor clients can host — token-level streaming, run-state reporting (state_update), permission bridging, and cancellation. Ships as an out-of-tree plugin: no fork of dsh, dependencies pinned to exact npm versions.
Maintained by ALwith; ALwith Desktop is its reference client (the desktop form of dsh). Upstream's own ACP surface is deliberately automation-only (fresh sessions, committed output only); this bridge provides the interactive side.
Status: developer preview. Covers
session/new+ prompt + streaming + run-state reporting + cancel +session/resume(live-first reattach, cold resume from JSONL persistence, client-drivenreplayFromhistory replay) + sandbox-first tools with escalation approvals + automatic context compaction (dsh-compaction-basic: step pressure and context-overflow recovery) with the human/compactcommand and tool-result pruning + delegation tools (subagent/subagent_fork/send_message/list_agents/interrupt_agentover in-process spawn and fork backends) + in-session model switching (session/set_config_option) + LLM session titles (deterministic first, one background generate upgrades) + all dsh presets (standard/minimal/anchored/codePTC on the official process runtime adapted for Bun /cordisruntime inspection with the read-only Cordis toolset and the bundledcordis-plugin-developmentskill) + host-facingplugins/sessionsmanagement CLIs + a multi-provider seat (ALWITH_DSH_PI_PROVIDERS: pi-ai catalog routes — openai / anthropic / google / xai / … — mounted beside the DeepSeek adapter).
Layout
| File | Responsibility |
|---|---|
src/bridge.ts | ACP v2 server plugin (adapted from upstream's automation-only v1 bridge, MIT) |
src/codec.ts | Pure wire-format translation (adapted from the upstream codec) |
src/plugins.ts | Plugin manifest: the composition as data (per-preset roster, core protection, overrides) |
src/compose.ts | Composes the runtime from the manifest (no dsh loader/profile — deterministic, runs on Bun) |
src/plugins-cli.ts | plugins subcommand: list and toggle plugins without a live session |
src/sessions-cli.ts | sessions subcommand: session-log inspection through dsh's own persistence |
skills/ | Skills bundled into presets (vendored from dsh's cordis preset, MIT) |
src/main.ts | stdio entry for hosts to spawn |
Run
Requires Bun (validated with 1.4.2); Node.js is not a supported sidecar runtime. In a repository checkout, install the pinned dependency set with bun install --frozen-lockfile.
DEEPSEEK_API_KEY=… bun src/main.ts # ACP v2 server over stdio
bun test # protocol tests with a mock adapter; no real model calls
session/resume semantics: an omitted replayFrom means context-only restore; { type: "start" } replays the whole conversation as session/update frames. Exactly one persistence provider is mounted per process, chosen by ALWITH_DSH_PERSISTENCE: dsh (default) keeps dsh's own JSONL logs under $ALWITH_DSH_SESSIONS_ROOT (default ~/.dsh-agent/sessions); alwith stores each session as an ALwith session record ($ALWITH_DSH_PROJECTS_DIR/<project key>/<sessionId>.jsonl, plain JSONL compatible with Claude Code messages, with every dsh event kept verbatim). With alwith, session/resume also continues records the ALwith CLI wrote: their messages, tool calls and results are rebuilt into dsh events; thinking blocks never enter the model context. Both providers implement the dsh 0.2.1-rc.2 handle seam (single-writer ownership per session, live-event routing with a batched durability barrier, torn-tail repair before the first append, fail-closed vocabulary) and pass the upstream persistence contract suite.
session/prompt returns the user message's messageId as soon as the Harness inserts it into model-visible history, identical to its live user_message update and retained history. This receipt does not wait for generation or its durability checkpoint. A queued message cancelled before insertion rejects the request instead of fabricating a receipt. Clients correlate the receipt with the user update and wait for the subsequent idle event to detect completion. The wire contract is validated with ACP SDK 1.5.1.
Turn completion, including cancellation, waits for agent convergence and the session durability checkpoint before reporting idle. A tool that ignores cancellation can delay this boundary. Prompts arriving while cancellation settles are rejected; an empty prompt during an active turn does not end it. session/close drains pending events before releasing the agent and cancels background title work. Model and checkpoint failures report _error, so hosts can keep failures actionable.
Model changes are rejected while a prompt, another switch, or unconsumed input is pending. Prompts wait for an existing switch; closing the session during it prevents replacement publication. A switch flushes first, which materializes even an untouched session, so after a failed switch every session can be recovered with session/resume.
Cancel and close intentionally discard pending input. There is no private history-seeding extension: continuation is session/resume against the mounted persistence provider. Adapters must honor abort signals: otherwise cancellation, close and shutdown can stall. The host may enforce a process deadline, but forced termination can lose unflushed log entries and leave truncated history on resume. Background titles are aborted without delaying close. Standard ACP error codes are retained; human-readable error messages are not a stable parsing API.
From a repository checkout, for local artifact verification run python3 scripts/verify-package.py. The release and verifier use bun scripts/pack.ts, which restores bun.lock in the Bun-generated tarball. The verifier requires the extracted lock to match the repository bytes, records its SHA256, installs production dependencies frozen (network and native build prerequisites may be required), and runs the declared executable through credential-free ACP checks for all five presets. Its watchdogs belong to the verifier, not the bridge. Logs and artifacts are retained at the printed path; install failures remain failures. The packageManager field records the validated toolchain; it is not runtime enforcement. The published tarball carries the lock required by Desktop installation. This does not establish cross-platform or real-provider compatibility.
Plugins
The composition per preset is fixed here in code (deterministic), but users keep dsh's two degrees of freedom — per-plugin enable/disable and per-plugin config — through an overrides file ($ALWITH_DSH_PLUGINS_FILE, default ~/.dsh-agent/plugins.json), read at spawn so changes apply to new sessions:
bun src/main.ts plugins list --preset standard # every row of the preset, JSON
bun src/main.ts plugins set tool-web disabled # validated before writing
{ "disabled": ["tool-web"], "config": { "bash-sandbox": { "timeoutMs": 120000 } } }
Core rows (session, llm, sandbox, approvals, …) cannot be disabled, and disabling a row another enabled row requires is rejected with the exact fix — both fail loud before anything is written. config entries shallow-merge over the row defaults.
License
MIT; portions adapted from upstream @deepseek-ai/dsh-acp are noted in THIRD_PARTY_NOTICES.md.
Harness 0.2.1-rc.2
The entire Harness release set is pinned to 0.2.1-rc.2. DeepSeek API-key routing uses dsh-llm-deepseek-api-key; tool results use the current flat tool-role message contract. Cordis exposes cordis_inspect_list and cordis_inspect_query; removed authoring tools are not emulated.
The PTC host and child both run Bun. src/ptc-bun-loader.ts adapts the official process runtime with amaro type stripping and Bun-compatible inherited-pipe streams. No Node executable or old worker fork is required. File confinement and elapsed deadlines remain enforced; the upstream V8 old-generation heap setting is not enforced by Bun.
ACP SDK 2.0 and runtime composition
The bridge uses ACP SDK 2.0.0 (experimental/v2/unstable). Every preset mounts the working-directory service required by the current tool and subagent APIs. JSONL persistence uses the upstream module plugin, preserving its injection metadata; PTC uses the explicit launch configuration with Bun for both host and child. Harness packages and Cordis peers are pinned to the same upstream release set.