-
Notifications
You must be signed in to change notification settings - Fork 554
feat: add DeepSeek Harness driver (drive bots with dsh ACP) #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # DeepSeek Harness driver | ||
|
|
||
| OpenMausBot can drive DeepSeek models through | ||
| [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) | ||
| instead of the claude/codex/grok CLIs. Each bot is backed by a real DeepSeek | ||
| agent (e.g. `deepseek-v4-pro`), using the same `DEEPSEEK_API_KEY` that | ||
| `dsh web` already reads from `~/.dsh/.credentials.yaml` — no new account or | ||
| login. | ||
|
|
||
| ## How it works | ||
|
|
||
| DeepSeek Harness ships an automation-only | ||
| [Agent Client Protocol](https://agentclientprotocol.com) server | ||
| (`@deepseek-ai/dsh-acp`) over JSON-RPC stdio. The `deepseek` driver rides | ||
| OpenMausBot's generic ACP runtime (`server/drivers/acp/core.ts`); its "CLI" is | ||
| the bundled `deepseek-acp` launcher, which: | ||
|
|
||
| 1. reads `DEEPSEEK_API_KEY` from `~/.dsh/.credentials.yaml` (or the environment), | ||
| 2. boots a built `dsh` checkout's ACP server | ||
| (`packages/examples/acp-demo/lib/bin.js --config examples/acp-agent/cordis.yml`), | ||
| 3. forwards stdio so the ACP JSON-RPC stream reaches the driver unchanged. | ||
|
|
||
| ``` | ||
| bot message → OpenMausBot → deepseek driver → deepseek-acp → dsh ACP server → DeepSeek model | ||
| ``` | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - a **built** DeepSeek Harness checkout (`pnpm install && pnpm run build`), | ||
| pointed at by `DSH_HOME`; | ||
| - `DEEPSEEK_API_KEY` in `~/.dsh/.credentials.yaml` (the same file `dsh web` | ||
| uses) or in the environment; | ||
| - Node 24+ (the OpenMausBot baseline). | ||
|
|
||
| ## Configuration | ||
|
|
||
| Add a DeepSeek instance to `~/.openmausbot/config.json`: | ||
|
|
||
| ```json | ||
| { | ||
| "instances": { | ||
| "deepseek": { | ||
| "driver": "deepseek", | ||
| "config": { | ||
| "cli": "<repo>/server/drivers/acp/deepseek-acp.mjs", | ||
| "workspace": "/absolute/path/to/agent/cwd" | ||
| }, | ||
| "environment": { | ||
| "DSH_HOME": "/path/to/deepseek-harness" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| - `cli` — the launcher path (defaults to `deepseek-acp` on `PATH`). | ||
| - `workspace` — the agent's working directory; must be absolute. The `dsh` | ||
| agent's bash and filesystem tools are sandboxed to this directory. | ||
| - `DSH_HOME` — the built checkout; defaults to `~/deepseek-harness`. | ||
|
|
||
| ## Models | ||
|
|
||
| `deepseek-v4-pro` (default) and `deepseek-v4-flash`. A non-default model is | ||
| applied by deriving a config with the `model:` line swapped. The derived | ||
| config is written under the checkout's `examples/acp-agent/` (not `/tmp`) | ||
| because `dsh`'s pnpm layout resolves the `@deepseek-ai/*` workspace packages | ||
| relative to the config file's directory, not the process cwd. | ||
|
|
||
| ## Conversation history | ||
|
|
||
| `dsh`'s ACP server has no `session/load` — every turn is a fresh agent | ||
| session. The driver therefore replays the settled transcript inline (the same | ||
| shape as the `grok` API driver), so a bot remembers its conversation without | ||
| provider-side resume. The harness routes rewinds through that same | ||
| transcript-replay path for `deepseek`. | ||
|
|
||
| ## Limitations | ||
|
|
||
| - **No computer / connected-app tooling.** The driver advertises | ||
| `computerMcp`/`agentsMcp` as off because `dsh`'s ACP server rejects | ||
| non-empty `mcpServers`; the UI must never promise tooling the agent cannot | ||
| mount. Chat works fully. | ||
| - **Committed answers.** `dsh` streams committed assistant message blocks | ||
| rather than token-level deltas, so replies arrive at message granularity. | ||
| - **Model fixed at boot.** The ACP server resolves the model from its config; | ||
| switching a bot's model restarts the ACP server for that turn. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| #!/usr/bin/env node | ||
| // DeepSeek Harness ACP launcher for OpenMausBot. | ||
| // | ||
| // Boots DeepSeek Harness's automation ACP server (Agent Client Protocol over | ||
| // JSON-RPC stdio) and forwards stdio + exit status, so the OpenMausBot | ||
| // `deepseek` driver can drive DeepSeek agents exactly like the claude/codex/ | ||
| // grok CLIs. | ||
| // | ||
| // --version print one version line and exit (driver snapshot detection) | ||
| // --model id override the ACP agent model (default: deepseek-v4-pro) | ||
| // | ||
| // DEEPSEEK_API_KEY is read from the process environment, or from | ||
| // ~/.dsh/.credentials.yaml (the same file `dsh web` uses) when absent. | ||
| // DSH_HOME points at a built deepseek-harness checkout (defaults to | ||
| // ~/deepseek-harness). | ||
| import { spawn } from "node:child_process"; | ||
| import { existsSync, readFileSync, writeFileSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { join } from "node:path"; | ||
|
|
||
| const VERSION = "deepseek-harness-acp 0.1.0-rc.5"; | ||
| const DSH_HOME = process.env.DSH_HOME || join(homedir(), "deepseek-harness"); | ||
| const DEFAULT_MODEL = "deepseek-v4-pro"; | ||
|
|
||
| const args = process.argv.slice(2); | ||
| if (args.includes("--version")) { | ||
| console.log(VERSION); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| let model = DEFAULT_MODEL; | ||
| const mi = args.indexOf("--model"); | ||
| if (mi !== -1 && args[mi + 1]) model = args[mi + 1]; | ||
|
|
||
| if (!process.env.DEEPSEEK_API_KEY) { | ||
| const cred = join(homedir(), ".dsh", ".credentials.yaml"); | ||
| if (existsSync(cred)) { | ||
| try { | ||
| const m = readFileSync(cred, "utf8").match(/^\s*DEEPSEEK_API_KEY:\s*(.+?)\s*$/m); | ||
| if (m) process.env.DEEPSEEK_API_KEY = m[1].replace(/^["']|["']$/g, ""); | ||
| } catch { | ||
| /* unreadable credentials file — leave the key unset and let dsh fail loud */ | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const acpBin = join(DSH_HOME, "packages", "examples", "acp-demo", "lib", "bin.js"); | ||
| const stockConfig = join(DSH_HOME, "examples", "acp-agent", "cordis.yml"); | ||
|
|
||
| // The stock acp-agent config hardcodes `model: deepseek-v4-pro`. A different | ||
| // model needs a derived config with that one line swapped. It MUST live under | ||
| // examples/acp-agent/ — DSH's pnpm layout links the @deepseek-ai workspace | ||
| // packages under examples/node_modules, and the Loader resolves those from the | ||
| // config file's directory, not the process cwd. | ||
| let configPath = stockConfig; | ||
| if (model && model !== DEFAULT_MODEL) { | ||
| const safeModel = model.replace(/[^a-zA-Z0-9._-]/g, "-"); | ||
| const yml = readFileSync(stockConfig, "utf8").replace( | ||
| /^(\s*model:)\s*deepseek-v4-pro\s*$/m, | ||
| `$1 ${model}`, | ||
| ); | ||
| configPath = join(DSH_HOME, "examples", "acp-agent", `cordis.openmausbot-${safeModel}.yml`); | ||
| writeFileSync(configPath, yml); | ||
| } | ||
|
|
||
| const child = spawn(process.execPath, [acpBin, "--config", configPath], { | ||
| cwd: DSH_HOME, | ||
| env: process.env, | ||
| stdio: "inherit", | ||
| }); | ||
| child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0))); | ||
| child.on("error", (err) => { | ||
| process.stderr.write(`deepseek-acp: ${err.message}\n`); | ||
| process.exit(1); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| // DeepSeek Harness support — drives DeepSeek agents through the DeepSeek | ||
| // Harness (dsh) automation ACP server (Agent Client Protocol over JSON-RPC | ||
| // stdio). Unlike the CLI-based drivers, the "CLI" is the deepseek-acp | ||
| // launcher, which boots a built dsh checkout's ACP server and reads | ||
| // DEEPSEEK_API_KEY from ~/.dsh/.credentials.yaml (the same file `dsh web` | ||
| // uses) — no claude/codex/grok binary or login. | ||
| // | ||
| // The instance config points the driver at the launcher and checkout: | ||
| // { "instances": { "deepseek": { | ||
| // "driver": "deepseek", | ||
| // "config": { "cli": "<repo>/server/drivers/acp/deepseek-acp.mjs", | ||
| // "workspace": "<agent cwd>" }, | ||
| // "environment": { "DSH_HOME": "<built dsh checkout>" } } } } | ||
| import { existsSync, readFileSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { join } from "node:path"; | ||
|
|
||
| import { createAcpDriver, type AcpSupport } from "./core.ts"; | ||
|
|
||
| function dshCredentialsPath(): string { | ||
| return join(homedir(), ".dsh", ".credentials.yaml"); | ||
| } | ||
|
|
||
| function hasCredentials(env: Record<string, string | undefined>): boolean { | ||
| if (env.DEEPSEEK_API_KEY) return true; | ||
| try { | ||
| return ( | ||
| existsSync(dshCredentialsPath()) && | ||
| /^\s*DEEPSEEK_API_KEY:\s*\S/m.test(readFileSync(dshCredentialsPath(), "utf8")) | ||
| ); | ||
| } catch { | ||
| return false; | ||
| } | ||
|
Comment on lines
+24
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject empty quoted API keys consistently.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| const support: AcpSupport = { | ||
| driverKind: "deepseek", | ||
| displayName: "DeepSeek", | ||
| models: { | ||
| default: "deepseek-v4-pro", | ||
| options: [ | ||
| { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro" }, | ||
| { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash" }, | ||
| ], | ||
| }, | ||
| defaultCli: "deepseek-acp", | ||
| nativeSource: "deepseek.acp", | ||
| loginNote: | ||
| "DeepSeek Harness isn't configured — set DEEPSEEK_API_KEY in ~/.dsh/.credentials.yaml and DSH_HOME to a built dsh checkout", | ||
|
|
||
| install: { | ||
| command: { | ||
| darwin: | ||
| "git clone https://github.com/deepseek-ai/deepseek-harness.git ~/deepseek-harness && cd ~/deepseek-harness && pnpm install && pnpm run build", | ||
| linux: | ||
| "git clone https://github.com/deepseek-ai/deepseek-harness.git ~/deepseek-harness && cd ~/deepseek-harness && pnpm install && pnpm run build", | ||
| }, | ||
| docsUrl: "https://github.com/deepseek-ai/deepseek-harness", | ||
| needsNode: true, | ||
| }, | ||
|
Comment on lines
+46
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Install the launcher that Line 46 defaults to A provider created with the default configuration remains unavailable after the setup command completes. Ship a stable launcher executable, or populate the default 🤖 Prompt for AI Agents |
||
|
|
||
| // The DSH ACP server advertises no auth methods; the key rides the | ||
| // launcher's environment, so auth is always ambient. | ||
| pickAuthMethod: () => null, | ||
| authFailure: "continue", | ||
| isAuthenticated: hasCredentials, | ||
|
|
||
| // DSH ACP rejects non-empty mcpServers (session/new accepts only empty | ||
| // additionalDirectories and mcpServers), so computer/agents tooling must | ||
| // not be advertised for this driver. | ||
| capabilities: { agentsMcp: false, computerMcp: false }, | ||
|
|
||
| // dsh's ACP server has no session/load, so every turn is a fresh session. | ||
| // Replay the settled transcript inline (same shape as the grok API driver) | ||
| // instead of relying on provider-side resume. | ||
| buildPromptText: (turn) => { | ||
| const parts: string[] = []; | ||
| if (turn.system) parts.push(turn.system); | ||
| for (const m of turn.transcript ?? []) { | ||
| parts.push(`${m.role === "user" ? "User" : "Assistant"}: ${m.text}`); | ||
| } | ||
| parts.push(`User: ${turn.text}`); | ||
| return parts.join("\n\n"); | ||
| }, | ||
|
|
||
| spawnArgs: (_config, turn) => (turn.model ? ["--model", turn.model] : []), | ||
| }; | ||
|
|
||
| export const DeepSeekDriver = createAcpDriver(support); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Set a language for this fenced block.
Line 23 has no fence language. This triggers markdownlint MD040. Use
textfor the architecture diagram.Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 23-23: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Source: Linters/SAST tools