English | 中文
Managed Python extension child speaking newline-delimited JSON-RPC 2.0 over stdio.
PythonBridge spawns one dsh-ext serve child through the dsh-subprocess seam and exchanges one JSON object per line: requests carry {"jsonrpc":"2.0","id",...,"method",...,"params":{}} and responses echo the id with result or error. The child is spawned lazily on the first call; any settled child (clean exit included) is dropped so the next call respawns; disposal quiesces through stdin EOF before escalating to tree termination.
This package is both a plain library and a DSH Cordis plugin: when loaded as a plugin it registers the pythonBridge service so other plugins can create bridges without manually threading the subprocess seam.
Install from the private registry:
npm install @nelsonlongxiang/dsh-python-bridgeimport { PythonBridge, PythonBridgeError, DEFAULT_EOF_GRACE_MS, resolveEofGrace } from '@nelsonlongxiang/dsh-python-bridge'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
// Inside a Cordis plugin: `ctx` is the injected Context carrying the subprocess seam.
const ctx = {
subprocess: {
spawn: (_spec: SubprocessSpawnSpec): SubprocessHandle => {
throw new Error('not invoked in this snippet')
},
},
} as unknown as { subprocess: { spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle } }
const bridge = new PythonBridge({
argv: ['dsh-ext', 'serve'],
cwd: process.cwd(),
eofGraceMs: DEFAULT_EOF_GRACE_MS,
spawn: (spec) => ctx.subprocess.spawn(spec),
})
const compositions = await bridge.call('graph/list', {})A JSON-RPC error rejects with PythonBridgeError carrying the wire code, message, and data; transport or spawn failures reject with their underlying error and fail every in-flight call. The public helpers DEFAULT_EOF_GRACE_MS and resolveEofGrace(value, caller?) are the single source of the EOF-grace default and its validation: resolveEofGrace throws TypeError for non-finite, non-positive, or above-timer-ceiling values, and the pythonBridge service runs every eofGraceMs through it on create.
- A library ships a runtime API that other plugin packages
import. It declares nodsh.bundle:dsh plugin addinstalls it as a plain dependency and prints a one-time orientation warning, activating no layer. A library never inserts plugin rows. - A bundle (plugin) ships a configuration layer — a patch file that inserts or overrides plugin rows, declared through
dsh.bundle.patch. Only this format can be enabled by users directly; installing a plugin-shaped package without the declaration is the classic "installed but never activated" anomaly.
This package is both by design — the PythonBridge runtime API plus the pythonBridge host service row — so since 0.3.4 it declares dsh.bundle and dsh plugin add activates its layer automatically (verified from-scratch: the package joins dsh.profile.bundles and the row composes with no warning).
One row id, exactly one source. The loader hard-rejects a duplicate entry id at boot (verified: duplicate loader entry id: python-bridge crashes startup). If a composition already inserts the python-bridge row from another patch (precedent: @nelsonlongxiang/dsh-native-teams), that profile must NOT also install this package as a direct dependency — pick one source and remove the other; compositions should migrate to depending on this bundle and dropping their own python-bridge insert rows. History: 0.3.1 declared dsh.bundle while dsh-native-teams also inserted the row, and the collision crashed startup (rolled back in 0.3.2; 0.3.3 carried the revert plus fixes).
The bundle's own patch inserts exactly this row:
- insert:
- id: python-bridge
name: '@nelsonlongxiang/dsh-python-bridge'Other plugins can then inject pythonBridge:
import { PythonBridge, PythonBridgeError } from '@nelsonlongxiang/dsh-python-bridge'
export const name = 'my-python-consumer'
export const inject = ['pythonBridge']
export function apply(ctx: any): void {
const bridge = ctx.pythonBridge.create({
argv: ['python', '-m', 'my_backend', 'serve'],
cwd: process.cwd(),
eofGraceMs: DEFAULT_EOF_GRACE_MS,
})
ctx.effect(() => () => { void bridge.dispose() }, 'my-consumer: bridge teardown')
// Use `bridge.call(...)` to talk to the Python child.
}The caller owns the returned bridge and is responsible for disposing it.
Nothing from this transport: it carries host-side tool and service calls between the harness and the dsh-ext serve child. Consumers that route results into prompts own what the model sees.
None — the bridge contributes no prompt content.
None — the bridge contributes no request content.
- The child is single-threaded and process-local: a crash fails in-flight calls and the bridge respawns on the next call, with no auto-retry.
- One bridge owns one child; sharing one
dsh-ext servechild across domains is deferred until a third domain appears.