mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
92 lines
3.6 KiB
TypeScript
92 lines
3.6 KiB
TypeScript
/**
|
|
* 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 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`.
|
|
*
|
|
* @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'
|
|
// 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 this plugin's fiber 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 }
|
|
// The later transport callback must dispose this plugin's fiber, not its ambient context.
|
|
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, {
|
|
maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess,
|
|
})
|
|
|
|
// Share one exit task and attempt flush and disposal independently before exiting.
|
|
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())])
|
|
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')
|
|
}
|