| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192 |
- /**
- * SDK-facing JSON-RPC plugin over stdio. An external `cordis.yml` decides
- * whether to load it; see the single-executable 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-jsonrpc
- */
- import type { Context } from 'cordis'
- import type { Readable, Writable } from 'node:stream'
- import Schema from 'schemastery'
- import { JsonRpcLineTransport } from '@deepseek-ai/dsh-sdk-protocol'
- import { HarnessSdkServer } from './server.ts'
- export * from './server.ts'
- export const name = 'jsonrpc'
- // 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 seams. */
- 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 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, {
- 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) => {
- 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')
- }
|