Replace the TypeScript SDK's public arbitrary command/argv launch surface with the same-version dsh CLI, a named profile, ordered per-launch patches, optional process cwd, and explicit Harness home selection. Installed consumers use the built CLI; clean source checkouts use the package's src/bin.ts through an absolute tsx/esm loader and a source-only patch that omits build-generated Typert loading. Materialize explicit or inherited environments at spawn time, keep profile-internal patches below caller patches, and resolve every caller-relative path before the child starts. The SDK subagent provider validates its optional CLI and patch files at plugin load and requires an isolated absolute child home. Treat JSON-RPC initialize as the Loader-owned readiness point: Loader settlement joins entry imports, fiber lifecycle work, and synchronous effect registration, so the server needs no scheduler tick. A delayed profile entry registers a private adapter before initialize resolves, proving caller-supplied plugin routes are visible without fallback. Update sdk-app ownership, server diagnostics, TypeScript SDK examples, nested-loader coverage, and session-upload replay layering together. The following commit contains only the regenerated SDK transcripts, keeping this API and lifecycle change directly reviewable.
102 lines
4.3 KiB
TypeScript
102 lines
4.3 KiB
TypeScript
/**
|
|
* SDK-facing JSON-RPC plugin over stdio. The selected dsh profile decides
|
|
* whether to load it; see the single-launch Agent Note and package README.
|
|
* Stdout is reserved for protocol frames, so the tree must not load a stdout logger.
|
|
* This plugin answers `shutdown`, disposes the complete root runtime, and exits 0; the app bin
|
|
* owns EOF and signal exits. Keep named plugin exports with no default export so
|
|
* Loader `unwrapExports` preserves `name`, `inject`, `Config`, and `apply`.
|
|
*
|
|
* @module @deepseek-ai/dsh-sdk-jsonrpc-server
|
|
*/
|
|
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import type { Readable, Writable } from 'node:stream'
|
|
import Schema from '@deepseek-ai/schemastery'
|
|
import { JsonRpcLineTransport } from '@deepseek-ai/dsh-sdk-protocol'
|
|
import { HarnessSdkJsonRpcServer } from './server.ts'
|
|
|
|
export * from './server.ts'
|
|
|
|
export const name = 'sdk-jsonrpc-server'
|
|
// Only the agent factory is required; initialize reads the optional LLM seam with ctx.get().
|
|
export const inject = ['agents']
|
|
|
|
/** JSON-RPC deployment config plus runtime-only test hooks. */
|
|
export interface JsonRpcConfig {
|
|
/** Report max-token turn/subagent termination as a successful SDK result. */
|
|
maxTokensAsSuccess?: boolean
|
|
/** Transport input override; production uses `process.stdin`. */
|
|
input?: Readable
|
|
/** Transport output override; production uses `process.stdout`. */
|
|
output?: Writable
|
|
/** Process-exit override; production uses `process.exit`. */
|
|
exit?: (code: number) => void
|
|
}
|
|
|
|
export const Config: Schema<JsonRpcConfig> = Schema.object({
|
|
maxTokensAsSuccess: Schema.boolean().default(false),
|
|
})
|
|
|
|
/**
|
|
* Serve SDK requests over the configured streams. Effect disposal shuts down
|
|
* SDK-created agents and closes the transport. A `shutdown` response is flushed
|
|
* before the root runtime is disposed and the process exits 0; the app bin
|
|
* owns root-context disposal for EOF and signals.
|
|
*/
|
|
export function apply(ctx: Context, config: JsonRpcConfig): void {
|
|
// Cordis applies the schema default before invoking the plugin.
|
|
const resolvedConfig = config as JsonRpcConfig & { maxTokensAsSuccess: boolean }
|
|
// Protocol shutdown owns the complete runtime process, so it must await the
|
|
// root lifecycle (including persistence) before exiting.
|
|
const rootFiber = ctx.root.fiber
|
|
/* v8 ignore next -- production stdio wiring; tests always inject the runtime hooks */
|
|
const input = config.input ?? process.stdin
|
|
/* v8 ignore next -- production stdio wiring; tests always inject the runtime hooks */
|
|
const output = config.output ?? process.stdout
|
|
/* v8 ignore next -- production exit wiring; tests always inject the runtime hooks */
|
|
const exit = config.exit ?? ((code: number): void => { process.exit(code) })
|
|
|
|
const transport = new JsonRpcLineTransport(input, output)
|
|
const server = new HarnessSdkJsonRpcServer(ctx, transport, {
|
|
maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess,
|
|
})
|
|
|
|
// Share one exit task so racing shutdown requests cannot dispose the root or
|
|
// exit the process more than once.
|
|
let exitTask: Promise<void> | undefined
|
|
const disposeAndExit = (): Promise<void> => {
|
|
exitTask ??= (async () => {
|
|
await Promise.allSettled([Promise.resolve().then(() => transport.flush())])
|
|
await Promise.allSettled([Promise.resolve().then(() => rootFiber.dispose())])
|
|
exit(0)
|
|
})()
|
|
return exitTask
|
|
}
|
|
|
|
transport.onRequest(async (method, params) => {
|
|
// `initialize` is the SDK's readiness boundary. This plugin can activate
|
|
// before async sibling Loader entries (for example an MCP client's initial
|
|
// tool discovery), so do not advertise a ready runtime until the complete
|
|
// current tree has settled. Loader settlement joins entry imports, fiber
|
|
// lifecycle work, and synchronous effect registration; no scheduler delay
|
|
// is part of readiness. A hand-built context without Loader remains
|
|
// immediately usable.
|
|
if (method === 'initialize') {
|
|
await ctx.get('loader')?.await()
|
|
}
|
|
const result = await server.handleRequest(method, params)
|
|
if (method === 'shutdown') {
|
|
// Run after the handler result is written; the task then flushes, disposes, and exits.
|
|
setImmediate(() => { void disposeAndExit() })
|
|
}
|
|
return result
|
|
})
|
|
|
|
ctx.effect(() => {
|
|
transport.start()
|
|
return async () => {
|
|
await server.shutdown()
|
|
transport.close()
|
|
}
|
|
}, 'jsonrpc.serve')
|
|
}
|