pi-dsh
August 15, 2026 · View on GitHub
Pi Coding Agent provider extension that exposes the DeepSeek Harness (dsh) as a selectable provider. Selecting the dsh provider routes each turn to DSH's own harness, running an in-process cordis tree with a persistent per-conversation session pool instead of an HTTP LLM endpoint.
Context ownership = DSH. DSH natively loads its own system prompt, AGENTS.md (~/.dsh/AGENTS.md + the project AGENTS.md chain via dsh-agent-instructions), and skills (~/.agents/skills, ~/.dsh/skills, project .dsh/skills + .agents/skills via dsh-skill-filesystem). pi-dsh does not re-forward pi's AGENTS.md/skills — that would double-load.
Requirements
- A Pi Coding Agent host (
@earendil-works/pi-ai/@earendil-works/pi-coding-agent>= 0.82.0) - A real DSH install for pool mode (
~/.dsh/profiles/headless+ credentials);dshon PATH for the oneshot fallback - Node.js >= 22 for running the verification scripts (
test/verify-pathb.mjsimports a standalone.tsmodule via native type stripping)
How it works
- Registers a provider
dshwith a customapi: "dsh-exec"and a non-HTTPbaseUrl: "dsh-exec://local". - Pool mode (default,
mode: "pool"): boots one in-process DSH cordis tree per process (thedsh-basecomposition, with the HMR row disabled), then keeps a pool of live agents — one DSH conversation per Pi conversation, keyed by Pi's session id (LRU cap 8, 15-min idle TTL). Each turn isagent.followup(...), and thesession/eventfirehose is translated into Pi'sAssistantMessageEventStream—text_delta/thinking_delta, with tool activity shown as thinking blocks — so the answer streams live, not as one block. - One-shot fallback (
mode: "oneshot"): the pre-path-B behavior — spawndsh --profile headless "<user text>", collect stdout, emit one text block. Kept config-selectable in case the rc tree misbehaves. - DSH owns prompt/memory/skills; pi-dsh only routes the latest user message. Cross-turn continuity comes from DSH's session persistence (
~/.dsh/sessionsJSONL): a resumed Pi conversation resumes the same DSH session (via deterministic session ids, so continuity even survives a host restart).
Permissions
The embedded DSH tree boots with sandbox danger-full-access + approval never, patched at boot in src/dsh-host.ts (dsh-base would otherwise default to workspace-write + approval ask).
This is an install-time trust decision, not a per-action gate. DSH runs its own tool loop (bash/fs/subagents) inside the provider, and that activity is surfaced as thinking blocks — Pi's approval UI never sees or gates these calls, by design (forwarding them as Pi toolcall_* events would hand tool execution to Pi's agent loop and re-drive the turn forever). So once pi-dsh is installed, the harness acts with your full local access and nothing — neither Pi nor DSH — prompts before the model writes files or runs commands. This mirrors pi-claude-bridge's permissionMode: "bypassPermissions" and pi-cursor-sdk's SDK embedding (no permission dialogs at all).
The two settings are a pair, and only one of them is what actually lets tools run. danger-full-access permits everything at the sandbox layer, so nothing ever needs to escalate. Approval never does not mean "auto-approve" — dsh-user-approval resolves it to rejected — it means escalation requests die deterministically and the model is told up front not to attempt sandbox_permissions.
- DSH's file sandbox does not restrict writes from the embedded tree.
- Network/process policy is outside DSH's
SandboxModevocabulary in any mode. - Confining the tree (e.g.
workspace-write) requires bridging approval requests to Pi's permission UI first, and is a deliberate future change. Simply lowering the sandbox mode does not work:askinvites the model to escalate, but with no answerer mounted every request fails closed asunavailable, so it burns steps on retries that can never succeed.
Install
Published on npm:
pi install npm:pi-dsh
For local development, add the absolute path of this repo to
~/.pi/agent/settings.json → packages:
{
"packages": [
"<absolute-path-to-this-repo>"
]
}
Then restart pi (or your pi host). The dsh provider appears in /model as
dsh/<model> — the model DSH itself is configured to run
(~/.dsh/settings.yaml → agent-default-model, e.g. dsh/deepseek-v4-pro).
When that file is unreadable the catalog falls back to a synthetic
dsh/dsh-harness entry. DSH owns model selection; pi-dsh mirrors it.
Dependency layout
@deepseek-ai/* are pinned (0.1.0-rc.6) pi-dsh dependencies and live in
this repo's node_modules as real installs, isolated from the pi host's
dependency graph (dsh pulls zod@4, which must not be hoisted over a host
that pins zod@3). The pi host packages and dev tooling resolve through
scope-level symlinks into the host install. See
plans/002-design-notes.md for the exact layout and rationale.
Development
npm install # installs pinned @deepseek-ai/* deps + dev tooling
# point the pi host packages at your pi install (single physical pi-ai instance):
ln -s <your-pi-install>/node_modules/@earendil-works node_modules/@earendil-works
npm run typecheck # tsc --noEmit
npm run verify # provider registration check
npm run verify:stream # oneshot-mode regression (fake dsh)
npm run verify:pathb # pool-mode integration (real dsh; SKIPs if absent)
Configure (optional)
~/.pi/agent/dsh.json:
{
"mode": "pool",
"poolMax": 8,
"poolIdleTtlMs": 900000,
"dshBin": "dsh",
"timeoutMs": 1800000
}
mode:"pool"(default) or"oneshot".poolMax: max pooled sessions (LRU-evicted beyond this). Default 8.poolIdleTtlMs: idle time before a pooled session closes, ms. Default 15 min.dshBin: used only by the"oneshot"fallback. Defaultdsh.timeoutMs: used only by the"oneshot"fallback (per-run budget). Default 30 min.
Env overrides: PI_DSH_MODE, PI_DSH_POOL_MAX, PI_DSH_POOL_IDLE_TTL_MS, PI_DSH_BIN, PI_DSH_TIMEOUT_MS.
Pinning policy
The in-process DSH surface is unstable (0.1.0-rc.6, SESSION_FORMAT_VERSION = 0, "no compatibility is implied"). @deepseek-ai/* are pinned to exact versions in package.json (no ^), and every @deepseek-ai/* import is isolated behind src/dsh-host.ts (the single chokepoint). Any dsh upgrade (0.1.0-rc.x → newer) requires re-running the Phase 1–2 spike gates (recorded in the repo's plans/ notes — not shipped in the npm tarball) before bumping the pins — the assistant/chunk / turn/end event vocabulary has no compat guarantee.
Limitations
- Pool mode requires a real DSH install (
~/.dsh/profiles/headless+ credentials in~/.dsh/.credentials.yaml). If the rc tree misbehaves, setmode: "oneshot"to fall back to the stateless subprocess path (no continuity, no streaming). - Abort cancels the live turn but preserves the pooled session; an entry is destroyed only on a real turn error or by the idle-TTL/LRU sweeper.
- Tool calls run inside DSH's own tool loop and appear as thinking blocks AT their chronological position (
[tool] …on call,[tool result] …on completion) — so the stream interleaves by time (think → tool → think → tool) instead of piling tools into a separate area. They are never Pitoolcall_*events: foreign toolCall blocks make Pi's agent loop take over tool execution with empty toolResults and re-drive the turn (an infinite loop). - Usage is reported per turn: DSH's
usagechunks are per model call (verified against persisted sessions), so pi-dsh sums the turn's chunks;totalTokensstays prompt-side so Pi's auto-compaction never misfires.
Verification
npx tsc --noEmit # typecheck
node verify.mjs # provider registered
node verify-stream.mjs # path-A (oneshot) regression vs fake-dsh.sh
node test/verify-pathb.mjs # path-B pool integration (real dsh; SKIP if absent)