index.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. /**
  2. * SDK-facing JSON-RPC plugin over stdio. An external `cordis.yml` decides
  3. * whether to load it; see the single-executable RFC 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. /** Runtime-only test seams; no field is configurable from `cordis.yml`. */
  22. export interface JsonRpcConfig {
  23. /** Transport input override; production uses `process.stdin`. */
  24. input?: Readable
  25. /** Transport output override; production uses `process.stdout`. */
  26. output?: Writable
  27. /** Process-exit override; production uses `process.exit`. */
  28. exit?: (code: number) => void
  29. }
  30. export const Config: Schema<JsonRpcConfig> = Schema.object({})
  31. /**
  32. * Serve SDK requests over the configured streams. Effect disposal shuts down
  33. * SDK-created agents and closes the transport. A `shutdown` response is flushed
  34. * before this plugin's fiber is disposed and the process exits 0; the app bin
  35. * owns root-context disposal for EOF and signals.
  36. */
  37. export function apply(ctx: Context, config: JsonRpcConfig): void {
  38. // The later transport callback must dispose this plugin's fiber, not its ambient context.
  39. const fiber = ctx.fiber
  40. /* v8 ignore next -- production stdio wiring; tests always inject the runtime seams */
  41. const input = config.input ?? process.stdin
  42. /* v8 ignore next -- production stdio wiring; tests always inject the runtime seams */
  43. const output = config.output ?? process.stdout
  44. /* v8 ignore next -- production exit wiring; tests always inject the runtime seams */
  45. const exit = config.exit ?? ((code: number): void => { process.exit(code) })
  46. const transport = new JsonRpcLineTransport(input, output)
  47. const server = new HarnessSdkServer(ctx, transport)
  48. // Share one exit task and attempt flush and disposal independently before exiting.
  49. let exitTask: Promise<void> | undefined
  50. const disposeAndExit = (): Promise<void> => {
  51. exitTask ??= (async () => {
  52. await Promise.allSettled([Promise.resolve().then(() => transport.flush())])
  53. await Promise.allSettled([Promise.resolve().then(() => fiber.dispose())])
  54. exit(0)
  55. })()
  56. return exitTask
  57. }
  58. transport.onRequest(async (method, params) => {
  59. const result = await server.handleRequest(method, params)
  60. if (method === 'shutdown') {
  61. // Run after the handler result is written; the task then flushes, disposes, and exits.
  62. setImmediate(() => { void disposeAndExit() })
  63. }
  64. return result
  65. })
  66. ctx.effect(() => {
  67. transport.start()
  68. return async () => {
  69. await server.shutdown()
  70. transport.close()
  71. }
  72. }, 'jsonrpc.serve')
  73. }