返回源码地图

packages/sdk/server/src/index.ts

main snapshot · da00f7f5358f · 正文引用章节 14;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * SDK-facing JSON-RPC plugin over stdio. The selected dsh profile decides
3 * whether to load it; see the single-launch 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 the complete root runtime, 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-sdk-jsonrpc-server
10 */
11
12import type { Context } from '@deepseek-ai/cordis'
13import type { Readable, Writable } from 'node:stream'
14import Schema from '@deepseek-ai/schemastery'
15import { JsonRpcLineTransport } from '@deepseek-ai/dsh-sdk-protocol'
16import { HarnessSdkJsonRpcServer } from './server.ts'
17
18export * from './server.ts'
19
20export const name = 'sdk-jsonrpc-server'
21// Only the agent factory is required; initialize reads the optional LLM seam with ctx.get().
22export const inject = ['agents']
23
24/** JSON-RPC deployment config plus runtime-only test hooks. */
25export interface JsonRpcConfig {
26 /** Report max-token turn/subagent termination as a successful SDK result. */
27 maxTokensAsSuccess?: boolean
28 /** Transport input override; production uses `process.stdin`. */
29 input?: Readable
30 /** Transport output override; production uses `process.stdout`. */
31 output?: Writable
32 /** Process-exit override; production uses `process.exit`. */
33 exit?: (code: number) => void
34}
35
36export const Config: Schema<JsonRpcConfig> = Schema.object({
37 maxTokensAsSuccess: Schema.boolean().default(false),
38})
39
40/**
41 * Serve SDK requests over the configured streams. Effect disposal shuts down
42 * SDK-created agents and closes the transport. A `shutdown` response is flushed
43 * before the root runtime is disposed and the process exits 0; the app bin
44 * owns root-context disposal for EOF and signals.
45 */
46export function apply(ctx: Context, config: JsonRpcConfig): void {
47 // Cordis applies the schema default before invoking the plugin.
48 const resolvedConfig = config as JsonRpcConfig & { maxTokensAsSuccess: boolean }
49 // Protocol shutdown owns the complete runtime process, so it must await the
50 // root lifecycle (including persistence) before exiting.
51 const rootFiber = ctx.root.fiber
52 /* v8 ignore next -- production stdio wiring; tests always inject the runtime hooks */
53 const input = config.input ?? process.stdin
54 /* v8 ignore next -- production stdio wiring; tests always inject the runtime hooks */
55 const output = config.output ?? process.stdout
56 /* v8 ignore next -- production exit wiring; tests always inject the runtime hooks */
57 const exit = config.exit ?? ((code: number): void => { process.exit(code) })
58
59 const transport = new JsonRpcLineTransport(input, output)
60 const server = new HarnessSdkJsonRpcServer(ctx, transport, {
61 maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess,
62 })
63
64 // Share one exit task so racing shutdown requests cannot dispose the root or
65 // exit the process more than once.
66 let exitTask: Promise<void> | undefined
67 const disposeAndExit = (): Promise<void> => {
68 exitTask ??= (async () => {
69 await Promise.allSettled([Promise.resolve().then(() => transport.flush())])
70 await Promise.allSettled([Promise.resolve().then(() => rootFiber.dispose())])
71 exit(0)
72 })()
73 return exitTask
74 }
75
76 transport.onRequest(async (method, params) => {
77 // `initialize` is the SDK's readiness boundary. This plugin can activate
78 // before async sibling Loader entries (for example an MCP client's initial
79 // tool discovery), so do not advertise a ready runtime until the complete
80 // current tree has settled. Loader settlement joins entry imports, fiber
81 // lifecycle work, and synchronous effect registration; no scheduler delay
82 // is part of readiness. A hand-built context without Loader remains
83 // immediately usable.
84 if (method === 'initialize') {
85 await ctx.get('loader')?.await()
86 }
87 const result = await server.handleRequest(method, params)
88 if (method === 'shutdown') {
89 // Run after the handler result is written; the task then flushes, disposes, and exits.
90 setImmediate(() => { void disposeAndExit() })
91 }
92 return result
93 })
94
95 ctx.effect(() => {
96 transport.start()
97 return async () => {
98 await server.shutdown()
99 transport.close()
100 }
101 }, 'jsonrpc.serve')
102}