refactor(code-runtime): remove subprocess backend

This commit is contained in:
Tianyi Cui
2026-08-08 21:27:59 +08:00
parent c1d08edd83
commit 4d5345794d
44 changed files with 192 additions and 1614 deletions
@@ -6,7 +6,6 @@
*/
import { inspect } from 'node:util'
import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime'
import type { DoneMessage, ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts'
import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts'
import { decodeWorkerJson, encodeWorkerJson, snapshotCodeJsonValue } from './worker-json.ts'
@@ -311,7 +310,6 @@ export function wireReplies(port: BootstrapPort, pending: Map<number, PendingCal
* @param pending - the id-keyed map each posted call parks its handles in.
* @param nextId - the shared mutable id counter (worker-issued correlation ids).
* @param errorClasses - per-namespace constructors shared with program globals.
* @param maxFrameBytes - optional serialized transport cap checked before posting.
* @returns one namespace object per declaration, in declaration order.
*/
export function makeNamespaces(
@@ -320,7 +318,6 @@ export function makeNamespaces(
pending: Map<number, PendingCall>,
nextId: { value: number },
errorClasses: Map<string, BindingErrorConstructor> = makeBindingErrorClasses(data),
maxFrameBytes?: number,
): Record<string, unknown>[] {
return data.namespaces.map(({ global, names }) => {
const errorClass = errorClasses.get(global)
@@ -338,11 +335,6 @@ export function makeNamespaces(
if (detached === undefined) {
return Promise.reject(bindingFailure(errorClass, name, 'binding arguments must be lossless JSON'))
}
const call = { type: 'call' as const, id: nextId.value, global, name, args: encodeWorkerJson(detached) }
if (maxFrameBytes !== undefined
&& jsonValueBytesUpTo(call as unknown as CodeJsonValue, maxFrameBytes) === undefined) {
return Promise.reject(bindingFailure(errorClass, name, 'binding arguments exceed maxFrameBytes'))
}
return new Promise((resolve, reject) => {
const id = nextId.value++
pending.set(id, {
@@ -352,7 +344,7 @@ export function makeNamespaces(
},
})
try {
port.postMessage(call)
port.postMessage({ type: 'call', id, global, name, args: encodeWorkerJson(detached) })
} catch (error: unknown) {
pending.delete(id)
const message = `binding arguments must be structured-cloneable: ${error instanceof CapturedError ? error.message : String(error)}`
@@ -372,14 +364,12 @@ export function makeNamespaces(
* @param port - host message port or test double.
* @param data - the boot payload the host sent.
* @param streams - stdout/stderr objects captured as program logs.
* @param maxFrameBytes - optional serialized transport cap checked before posting.
* @returns after posting the done message.
*/
export async function runWorkerMain(
port: BootstrapPort,
data: WorkerBootData,
streams: { stdout: PatchableStream; stderr: PatchableStream },
maxFrameBytes?: number,
): Promise<void> {
const logs = new LogBuffer(
data.maxOutputBytes,
@@ -394,7 +384,7 @@ export async function runWorkerMain(
const nextId = { value: 1 }
const errorClasses = makeBindingErrorClasses(data)
const namespaces = makeNamespaces(data, port, pending, nextId, errorClasses, maxFrameBytes)
const namespaces = makeNamespaces(data, port, pending, nextId, errorClasses)
const errorClassParameters: string[] = []
const errorClassValues: BindingErrorConstructor[] = []
for (const namespace of data.namespaces) {
@@ -430,8 +420,5 @@ export async function runWorkerMain(
...prepareException(error, logs.remainingOutputBytes(), data.maxOutputBytes),
}
}
port.postMessage(maxFrameBytes !== undefined
&& jsonValueBytesUpTo(done as unknown as CodeJsonValue, maxFrameBytes) === undefined
? { type: 'output-limit' }
: done)
port.postMessage(done)
}
@@ -1,251 +0,0 @@
/** Shared host mechanics for local and subprocess-hosted TypeScript worker runtimes. */
import { stripTypeScriptTypes } from 'node:module'
import type { Readable } from 'node:stream'
import type {
CodeBindingNamespace,
CodeJsonValue,
CodeRunFailure,
CodeRunRequest,
CodeRunResult,
} from '@deepseek-ai/dsh-code-runtime'
import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts'
import { decodeWorkerJson, encodeWorkerJson, snapshotCodeJsonValue } from './worker-json.ts'
import type { WorkerJsonWire } from './worker-json.ts'
/** Smallest cap that can represent an empty log array and failure message. */
export const MIN_RUNTIME_OUTPUT_BYTES = 4
/**
* Resolve after a worker pipe emits queued data or closes during termination.
* @param stream - captured worker or child-process pipe.
* @returns after no more queued bytes can arrive.
*/
export function waitForRuntimePipeDrain(stream: Readable): Promise<void> {
if (stream.readableEnded || stream.destroyed) return Promise.resolve()
return new Promise((resolve) => {
const done = (): void => {
stream.off('end', done)
stream.off('close', done)
stream.off('error', done)
resolve()
}
stream.once('end', done)
stream.once('close', done)
stream.once('error', done)
/* v8 ignore next -- termination can win the adjacent listener-registration race. */
if (stream.readableEnded || stream.destroyed) done()
})
}
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
const RESERVED_WORDS = new Set([
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in',
'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package',
'private', 'protected', 'public', 'arguments', 'eval',
])
const RESERVED_ERROR_PROPERTIES = new Set(['name', 'message', 'stack'])
const STRIP_WRAP = { prefix: 'async function __dsh_program__() {\n', suffix: '\n}' } as const
/** One validated binding call received from an isolated worker. */
export interface RuntimeBindingCall {
/** Correlation id supplied by the isolated worker. */
readonly id: number
/** Injected namespace global. */
readonly global: string
/** Declared namespace function. */
readonly name: string
/** Untrusted lossless-JSON wire payload. */
readonly args: unknown
}
/** One host reply to an isolated worker binding call. */
export type RuntimeBindingReply =
| { readonly type: 'reply'; readonly id: number; readonly ok: true; readonly value: WorkerJsonWire }
| { readonly type: 'reply'; readonly id: number; readonly ok: false; readonly message: string }
/**
* Render an unknown thrown value without assuming it is an Error.
* @param error - thrown or rejected value.
* @returns the caller-facing diagnostic text.
*/
export function runtimeErrorMessage(error: unknown): string {
try {
return error instanceof Error ? error.message : String(error)
} catch {
return 'binding rejected with an unrenderable value'
}
}
/**
* Strip erasable TypeScript while preserving the program's body coordinates.
* @param program - model-written async-function body.
* @returns JavaScript source with the wrapper removed.
*/
export function stripRuntimeProgram(program: string): string {
const stripped = stripTypeScriptTypes(STRIP_WRAP.prefix + program + STRIP_WRAP.suffix)
return stripped.slice(STRIP_WRAP.prefix.length, stripped.length - STRIP_WRAP.suffix.length)
}
/**
* Validate binding globals and typed-error declarations shared by worker runtimes.
* @param request - code-runtime request carrying the namespaces.
* @param implementationName - package name used in seam-misuse diagnostics.
* @returns namespaces indexed by their injected global.
*/
export function validateRuntimeBindings(
request: CodeRunRequest,
implementationName: string,
): Map<string, CodeBindingNamespace> {
const bindings = new Map<string, CodeBindingNamespace>()
for (const namespace of request.bindings) {
if (!IDENTIFIER.test(namespace.global) || RESERVED_WORDS.has(namespace.global)) {
throw new Error(`${implementationName}: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`)
}
if (namespace.global === 'console' || bindings.has(namespace.global)) {
throw new Error(`${implementationName}: duplicate binding global ${JSON.stringify(namespace.global)}`)
}
bindings.set(namespace.global, namespace)
}
const errorClassNames = new Set<string>()
for (const namespace of request.bindings) {
const descriptor = namespace.errorClass
if (descriptor === undefined) continue
if (!IDENTIFIER.test(descriptor.name) || RESERVED_WORDS.has(descriptor.name)) {
throw new Error(`${implementationName}: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`)
}
if (descriptor.name === 'console' || bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) {
throw new Error(`${implementationName}: duplicate injected global ${JSON.stringify(descriptor.name)}`)
}
if (descriptor.memberNameProperty.length === 0 || RESERVED_ERROR_PROPERTIES.has(descriptor.memberNameProperty)) {
throw new Error(`${implementationName}: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`)
}
errorClassNames.add(descriptor.name)
}
return bindings
}
/**
* Resolve one untrusted worker call through a declared host binding.
* @param call - parsed call envelope from the isolated worker.
* @param bindings - namespaces returned by {@link validateRuntimeBindings}.
* @returns a lossless-JSON success or stable rejection reply.
*/
export async function invokeRuntimeBinding(
call: RuntimeBindingCall,
bindings: ReadonlyMap<string, CodeBindingNamespace>,
): Promise<RuntimeBindingReply> {
const functions = bindings.get(call.global)?.functions
const fn = functions !== undefined && Object.hasOwn(functions, call.name) ? functions[call.name] : undefined
if (typeof fn !== 'function') {
return { type: 'reply', id: call.id, ok: false, message: `unknown binding ${JSON.stringify(`${call.global}.${call.name}`)}` }
}
const args = decodeWorkerJson(call.args)
if (args === undefined) {
return { type: 'reply', id: call.id, ok: false, message: 'binding arguments must be lossless JSON' }
}
try {
const resolved = await fn(args)
let value: CodeJsonValue | undefined
try {
value = snapshotCodeJsonValue(resolved)
} catch {
value = undefined
}
if (value === undefined) {
return { type: 'reply', id: call.id, ok: false, message: 'binding resolution must be lossless JSON' }
}
return { type: 'reply', id: call.id, ok: true, value: encodeWorkerJson(value) }
} catch (error: unknown) {
return { type: 'reply', id: call.id, ok: false, message: runtimeErrorMessage(error) }
}
}
/** One run's combined outer-output ledger; binding values never enter it. */
export class RuntimeOutputLedger {
private bytes = 2
private entries = 0
/** @param maxBytes - hard cap for logs plus completion or failure payload. */
constructor(private readonly maxBytes: number) {}
/**
* Admit one exact log entry.
* @param text - candidate log entry.
* @param sink - ordered retained log list.
* @returns false when the hard cap was crossed.
*/
admit(text: string, sink: string[]): boolean {
const separatorBytes = this.entries > 0 ? 1 : 0
const stringBytes = jsonStringBytesUpTo(text, this.maxBytes - this.bytes - separatorBytes)
if (stringBytes === undefined) return false
this.bytes += stringBytes + separatorBytes
this.entries += 1
sink.push(text)
return true
}
/**
* Finalize a successful completion against the combined cap.
* @param logs - retained ordered logs.
* @param value - optional lossless-JSON completion.
* @returns the completion or output-limit result.
*/
success(logs: string[], value?: CodeJsonValue): CodeRunResult {
if (value !== undefined && jsonValueBytesUpTo(value, this.maxBytes - this.bytes) === undefined) return this.limit(logs)
return { logs, ...value !== undefined ? { value } : {} }
}
/**
* Finalize one failure diagnostic against the combined cap.
* @param logs - retained ordered logs.
* @param error - structured runtime failure.
* @returns the failure or output-limit result.
*/
failure(logs: string[], error: CodeRunFailure): CodeRunResult {
if (jsonStringBytesUpTo(error.message, this.maxBytes - this.bytes) === undefined) return this.limit(logs)
return { logs, error }
}
/**
* Build an explicit output-limit failure with a fitting log prefix.
* @param logs - ordered logs observed before the limit.
* @returns bounded output-limit result.
*/
limit(logs: string[]): CodeRunResult {
const fullMessage = `outer output exceeded ${this.maxBytes} bytes`
const messageBytes = fullMessage.length + 2
const retained: string[] = []
let retainedBytes = 2
const logBudget = this.maxBytes - messageBytes
for (const text of logs) {
const separatorBytes = retained.length > 0 ? 1 : 0
const availableBytes = logBudget - retainedBytes - separatorBytes
const stringBytes = jsonStringBytesUpTo(text, availableBytes)
if (stringBytes !== undefined) {
retained.push(text)
retainedBytes += stringBytes + separatorBytes
continue
}
const prefix = truncateJsonStringBytes(text, availableBytes)
if (prefix.length > 0) {
const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes)
/* v8 ignore next -- truncateJsonStringBytes guarantees the same bound. */
if (prefixBytes === undefined) throw new Error('output ledger produced an oversized log prefix')
retained.push(prefix)
retainedBytes += prefixBytes + separatorBytes
}
break
}
const message = truncateJsonStringBytes(fullMessage, this.maxBytes - retainedBytes)
return { logs: retained, error: { kind: 'output-limit', message } }
}
}
export { decodeWorkerJson, encodeWorkerJson, snapshotCodeJsonValue } from './worker-json.ts'
export { jsonStringBytesUpTo, jsonValueBytesUpTo } from './output-json.ts'
export { runWorkerMain } from './bootstrap.ts'
export type { WorkerJsonWire } from './worker-json.ts'
@@ -286,23 +286,6 @@ describe('makeNamespaces', () => {
expect(nextId.value).toBe(1)
})
it('rejects an oversized transport frame before posting or allocating a call id', async () => {
const port = new FakePort()
const pending = new Map<number, PendingCall>()
const nextId = { value: 1 }
const data = { namespaces: [toolNamespace(['x'])] }
const [tools] = makeNamespaces(
data, port, pending, nextId, makeBindingErrorClasses(data), 64,
) as [Record<string, (args: unknown) => Promise<unknown>>]
await expect(tools.x?.({ text: 'x'.repeat(64) })).rejects.toMatchObject({
name: 'ToolCallError', toolName: 'x', message: 'binding arguments exceed maxFrameBytes',
})
expect(port.sent).toEqual([])
expect(pending.size).toBe(0)
expect(nextId.value).toBe(1)
})
it('uses ordinary Error for non-tools namespace failures', async () => {
const deniedPort = new FakePort()
deniedPort.respond = message => message.type === 'call'
@@ -360,16 +343,6 @@ describe('runWorkerMain', () => {
})
})
it('reports output-limit before posting a completion that expands past the transport cap', async () => {
const port = new FakePort()
await runWorkerMain(port, {
maxOutputBytes: 1_000,
code: 'return Array.from({ length: 100 }, () => [])',
namespaces: [],
}, fakeStreams(), 100)
expect(port.sent.at(-1)).toEqual({ type: 'output-limit' })
})
it('reports a thrown program error on the done message', async () => {
const port = new FakePort()
await runWorkerMain(port, { ...BOOT, code: 'throw new Error("boom")', namespaces: [] }, fakeStreams())
@@ -8,6 +8,9 @@
"src"
],
"references": [
{
"path": "../../core/session"
},
{
"path": "../../../vendor/cosmokit"
},
@@ -1,10 +1,9 @@
import { defineConfig } from 'tsdown'
/**
* Build the plugin, reusable runtime host, and worker as separate bundles. The
* sibling `worker.cjs` is loaded by file and must be CommonJS for pkg's VFS
* Worker hook. Separate builds inline shared implementation instead of
* emitting an unlisted chunk outside the exact `files` whitelist.
* Build the index and worker as separate single-entry bundles. The sibling `worker.cjs` is loaded
* by file and must be CommonJS for pkg's VFS Worker hook. A multi-entry build emits an unlisted
* shared chunk omitted by the package's exact `files` whitelist; separate builds inline it.
*/
export default defineConfig([
{
@@ -17,16 +16,6 @@ export default defineConfig([
dts: false,
clean: false,
},
{
entry: ['lib/types/runtime-host.js'],
outDir: 'lib',
format: ['esm'],
platform: 'node',
target: 'es2024',
fixedExtension: false,
dts: false,
clean: false,
},
{
entry: ['lib/types/worker.js'],
outDir: 'lib',