1
/**2
* SDK-facing JSON-RPC plugin over stdio. The selected dsh profile decides3
* whether to load it; see the single-launch Agent Note and package README.4
* Stdout is reserved for protocol frames, so the tree must not load a stdout logger.5
* This plugin answers `shutdown`, disposes the complete root runtime, and exits 0; the app bin6
* owns EOF and signal exits. Keep named plugin exports with no default export so7
* Loader `unwrapExports` preserves `name`, `inject`, `Config`, and `apply`.8
*9
* @module @deepseek-ai/dsh-sdk-jsonrpc-server10
*/12
import type { Context } from '@deepseek-ai/cordis'13
import type { Readable, Writable } from 'node:stream'14
import Schema from '@deepseek-ai/schemastery'15
import { JsonRpcLineTransport } from '@deepseek-ai/dsh-sdk-protocol'16
import { HarnessSdkJsonRpcServer } from './server.ts'18
export * from './server.ts'20
export const name = 'sdk-jsonrpc-server'21
// Only the agent factory is required; initialize reads the optional LLM seam with ctx.get().22
export const inject = ['agents']24
/** JSON-RPC deployment config plus runtime-only test hooks. */25
export interface JsonRpcConfig {26
/** Report max-token turn/subagent termination as a successful SDK result. */27
maxTokensAsSuccess?: boolean28
/** Transport input override; production uses `process.stdin`. */29
input?: Readable30
/** Transport output override; production uses `process.stdout`. */31
output?: Writable32
/** Process-exit override; production uses `process.exit`. */33
exit?: (code: number) => void34
}36
export const Config: Schema<JsonRpcConfig> = Schema.object({37
maxTokensAsSuccess: Schema.boolean().default(false),38
})40
/**41
* Serve SDK requests over the configured streams. Effect disposal shuts down42
* SDK-created agents and closes the transport. A `shutdown` response is flushed43
* before the root runtime is disposed and the process exits 0; the app bin44
* owns root-context disposal for EOF and signals.45
*/46
export function apply(ctx: Context, config: JsonRpcConfig): void {47
// Cordis applies the schema default before invoking the plugin.48
const resolvedConfig = config as JsonRpcConfig & { maxTokensAsSuccess: boolean }49
// Protocol shutdown owns the complete runtime process, so it must await the50
// root lifecycle (including persistence) before exiting.51
const rootFiber = ctx.root.fiber52
/* v8 ignore next -- production stdio wiring; tests always inject the runtime hooks */53
const input = config.input ?? process.stdin54
/* v8 ignore next -- production stdio wiring; tests always inject the runtime hooks */55
const output = config.output ?? process.stdout56
/* v8 ignore next -- production exit wiring; tests always inject the runtime hooks */57
const exit = config.exit ?? ((code: number): void => { process.exit(code) })59
const transport = new JsonRpcLineTransport(input, output)60
const server = new HarnessSdkJsonRpcServer(ctx, transport, {61
maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess,62
})64
// Share one exit task so racing shutdown requests cannot dispose the root or65
// exit the process more than once.66
let exitTask: Promise<void> | undefined67
const disposeAndExit = (): Promise<void> => {68
exitTask ??= (async () => {69
await Promise.allSettled([Promise.resolve().then(() => transport.flush())])70
await Promise.allSettled([Promise.resolve().then(() => rootFiber.dispose())])71
exit(0)72
})()73
return exitTask74
}76
transport.onRequest(async (method, params) => {77
// `initialize` is the SDK's readiness boundary. This plugin can activate78
// before async sibling Loader entries (for example an MCP client's initial79
// tool discovery), so do not advertise a ready runtime until the complete80
// current tree has settled. Loader settlement joins entry imports, fiber81
// lifecycle work, and synchronous effect registration; no scheduler delay82
// is part of readiness. A hand-built context without Loader remains83
// immediately usable.84
if (method === 'initialize') {85
await ctx.get('loader')?.await()86
}87
const result = await server.handleRequest(method, params)88
if (method === 'shutdown') {89
// Run after the handler result is written; the task then flushes, disposes, and exits.90
setImmediate(() => { void disposeAndExit() })91
}92
return result93
})95
ctx.effect(() => {96
transport.start()97
return async () => {98
await server.shutdown()99
transport.close()100
}101
}, 'jsonrpc.serve')102
}