index.ts 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. /**
  2. * SDK-facing JSON-RPC plugin over stdio. An external `cordis.yml` decides
  3. * whether to load it; see the single-executable 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 its own fiber, and exits 0; the app bin
  6. * owns EOF and signal exits. Keep named plugin exports with no default export so
  7. * Loader `unwrapExports` preserves `name`, `inject`, `Config`, and `apply`.
  8. *
  9. * @module @deepseek-ai/dsh-jsonrpc
  10. */
  11. import type { Context } from 'cordis'
  12. import type { Readable, Writable } from 'node:stream'
  13. import Schema from 'schemastery'
  14. import { HarnessSdkServer } from './server.ts'
  15. import { JsonRpcLineTransport } from './transport.ts'
  16. export * from './server.ts'
  17. export * from './transport.ts'
  18. export const name = 'jsonrpc'
  19. // Only the agent factory is required; initialize reads the optional LLM seam with ctx.get().
  20. export const inject = ['agents']
  21. /** JSON-RPC deployment config plus runtime-only test seams. */
  22. export interface JsonRpcConfig {
  23. /** Report max-token turn/subagent termination as a successful SDK result. */
  24. maxTokensAsSuccess?: boolean
  25. /** Transport input override; production uses `process.stdin`. */
  26. input?: Readable
  27. /** Transport output override; production uses `process.stdout`. */
  28. output?: Writable
  29. /** Process-exit override; production uses `process.exit`. */
  30. exit?: (code: number) => void
  31. }
  32. export const Config: Schema<JsonRpcConfig> = Schema.object({
  33. maxTokensAsSuccess: Schema.boolean().default(false),
  34. })
  35. /**
  36. * Serve SDK requests over the configured streams. Effect disposal shuts down
  37. * SDK-created agents and closes the transport. A `shutdown` response is flushed
  38. * before this plugin's fiber is disposed and the process exits 0; the app bin
  39. * owns root-context disposal for EOF and signals.
  40. */
  41. export function apply(ctx: Context, config: JsonRpcConfig): void {
  42. // Cordis applies the schema default before invoking the plugin.
  43. const resolvedConfig = config as JsonRpcConfig & { maxTokensAsSuccess: boolean }
  44. // The later transport callback must dispose this plugin's fiber, not its ambient context.
  45. const fiber = ctx.fiber
  46. /* v8 ignore next -- production stdio wiring; tests always inject the runtime seams */
  47. const input = config.input ?? process.stdin
  48. /* v8 ignore next -- production stdio wiring; tests always inject the runtime seams */
  49. const output = config.output ?? process.stdout
  50. /* v8 ignore next -- production exit wiring; tests always inject the runtime seams */
  51. const exit = config.exit ?? ((code: number): void => { process.exit(code) })
  52. const transport = new JsonRpcLineTransport(input, output)
  53. const server = new HarnessSdkServer(ctx, transport, {
  54. maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess,
  55. })
  56. // Share one exit task and attempt flush and disposal independently before exiting.
  57. let exitTask: Promise<void> | undefined
  58. const disposeAndExit = (): Promise<void> => {
  59. exitTask ??= (async () => {
  60. await Promise.allSettled([Promise.resolve().then(() => transport.flush())])
  61. await Promise.allSettled([Promise.resolve().then(() => fiber.dispose())])
  62. exit(0)
  63. })()
  64. return exitTask
  65. }
  66. transport.onRequest(async (method, params) => {
  67. const result = await server.handleRequest(method, params)
  68. if (method === 'shutdown') {
  69. // Run after the handler result is written; the task then flushes, disposes, and exits.
  70. setImmediate(() => { void disposeAndExit() })
  71. }
  72. return result
  73. })
  74. ctx.effect(() => {
  75. transport.start()
  76. return async () => {
  77. await server.shutdown()
  78. transport.close()
  79. }
  80. }, 'jsonrpc.serve')
  81. }