2026-07-11 14:08:12 +08:00
|
|
|
/**
|
2026-07-14 00:40:36 +08:00
|
|
|
* SDK-facing JSON-RPC plugin over stdio. An external `cordis.yml` decides
|
|
|
|
|
* whether to load it; see the single-executable RFC and package README.
|
|
|
|
|
* Stdout is reserved for protocol frames, so the tree must not load a stdout logger.
|
|
|
|
|
* This plugin answers `shutdown`, disposes its own fiber, 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`.
|
2026-07-11 14:08:12 +08:00
|
|
|
*
|
|
|
|
|
* @module @deepseek-ai/dsh-jsonrpc
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type { Context } from 'cordis'
|
|
|
|
|
import type { Readable, Writable } from 'node:stream'
|
|
|
|
|
import Schema from 'schemastery'
|
|
|
|
|
import { HarnessSdkServer } from './server.ts'
|
|
|
|
|
import { JsonRpcLineTransport } from './transport.ts'
|
|
|
|
|
|
|
|
|
|
export * from './server.ts'
|
|
|
|
|
export * from './transport.ts'
|
|
|
|
|
|
|
|
|
|
export const name = 'jsonrpc'
|
2026-07-14 00:40:36 +08:00
|
|
|
// Only the agent factory is required; initialize reads the optional LLM seam with ctx.get().
|
2026-07-11 14:08:12 +08:00
|
|
|
export const inject = ['agents']
|
|
|
|
|
|
2026-07-14 00:40:36 +08:00
|
|
|
/** Runtime-only test seams; no field is configurable from `cordis.yml`. */
|
2026-07-11 14:08:12 +08:00
|
|
|
export interface JsonRpcConfig {
|
2026-07-14 00:40:36 +08:00
|
|
|
/** Transport input override; production uses `process.stdin`. */
|
2026-07-11 14:08:12 +08:00
|
|
|
input?: Readable
|
2026-07-14 00:40:36 +08:00
|
|
|
/** Transport output override; production uses `process.stdout`. */
|
2026-07-11 14:08:12 +08:00
|
|
|
output?: Writable
|
2026-07-14 00:40:36 +08:00
|
|
|
/** Process-exit override; production uses `process.exit`. */
|
2026-07-11 14:08:12 +08:00
|
|
|
exit?: (code: number) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const Config: Schema<JsonRpcConfig> = Schema.object({})
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-14 00:40:36 +08:00
|
|
|
* Serve SDK requests over the configured streams. Effect disposal shuts down
|
|
|
|
|
* SDK-created agents and closes the transport. A `shutdown` response is flushed
|
|
|
|
|
* before this plugin's fiber is disposed and the process exits 0; the app bin
|
|
|
|
|
* owns root-context disposal for EOF and signals.
|
2026-07-11 14:08:12 +08:00
|
|
|
*/
|
|
|
|
|
export function apply(ctx: Context, config: JsonRpcConfig): void {
|
2026-07-14 00:40:36 +08:00
|
|
|
// The later transport callback must dispose this plugin's fiber, not its ambient context.
|
2026-07-11 14:08:12 +08:00
|
|
|
const fiber = ctx.fiber
|
|
|
|
|
/* v8 ignore next -- production stdio wiring; tests always inject the runtime seams */
|
|
|
|
|
const input = config.input ?? process.stdin
|
|
|
|
|
/* v8 ignore next -- production stdio wiring; tests always inject the runtime seams */
|
|
|
|
|
const output = config.output ?? process.stdout
|
|
|
|
|
/* v8 ignore next -- production exit wiring; tests always inject the runtime seams */
|
|
|
|
|
const exit = config.exit ?? ((code: number): void => { process.exit(code) })
|
|
|
|
|
|
|
|
|
|
const transport = new JsonRpcLineTransport(input, output)
|
|
|
|
|
const server = new HarnessSdkServer(ctx, transport)
|
|
|
|
|
|
2026-07-14 00:40:36 +08:00
|
|
|
// Share one exit task and attempt flush and disposal independently before exiting.
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
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(() => fiber.dispose())])
|
2026-07-11 14:08:12 +08:00
|
|
|
exit(0)
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
})()
|
|
|
|
|
return exitTask
|
2026-07-11 14:08:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
transport.onRequest(async (method, params) => {
|
|
|
|
|
const result = await server.handleRequest(method, params)
|
|
|
|
|
if (method === 'shutdown') {
|
2026-07-14 00:40:36 +08:00
|
|
|
// Run after the handler result is written; the task then flushes, disposes, and exits.
|
2026-07-11 14:08:12 +08:00
|
|
|
setImmediate(() => { void disposeAndExit() })
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
ctx.effect(() => {
|
|
|
|
|
transport.start()
|
|
|
|
|
return async () => {
|
|
|
|
|
await server.shutdown()
|
|
|
|
|
transport.close()
|
|
|
|
|
}
|
|
|
|
|
}, 'jsonrpc.serve')
|
|
|
|
|
}
|