66
this.name = 'JsonRpcError';
67
}
69
>
70
>
// #region Typed method projections
71
>
//
72
>
// Extract `<method>` → `params` / `result` for each direction. The
73
>
// generated unions have shape `{ method: "x/y", id?: RequestId, params: P
74
>
// }`, so a discriminated-union pick works as a method-keyed lookup.
75
>
76
>
type MethodOf<U> = U extends { method: infer M } ? M : never;
77
>
type ParamsOf<U, M> = U extends { method: M; params: infer P } ? P : never;
78
>
79
>
export type ClientRequestMethod = MethodOf<ClientRequest>;
80
>
export type ClientNotificationMethod = MethodOf<ClientNotification>;
81
>
export type ServerRequestMethod = MethodOf<ServerRequest>;
82
>
export type ServerNotificationMethod = MethodOf<ServerNotification>;
83
>
84
>
export type ClientRequestParams<M extends ClientRequestMethod> = ParamsOf<ClientRequest, M>;
85
>
export type ClientNotificationParams<M extends ClientNotificationMethod> = ParamsOf<ClientNotification, M>;
86
>
export type ServerRequestParams<M extends ServerRequestMethod> = ParamsOf<ServerRequest, M>;
87
>
export type ServerNotificationParams<M extends ServerNotificationMethod> = ParamsOf<ServerNotification, M>;
88
>
89
>
// `result` for client-issued requests doesn't have a single generated
90
>
// union; each method has its own `<X>Response` type. We surface the
91
>
// response shape as a generic parameter on `request<M, R>` so callers
92
>
// can name the response type explicitly, defaulting to `unknown`.
93
>
94
>
// #endregion
95
>
96
>
/**
97
>
* Result of a server→client request. Either a successful result payload
98
>
* or a JSON-RPC error envelope. Implementations of
99
>
* {@link ICodexAppServerClient.onRequest} return one of these.
100
>
*/
101
>
export type ServerRequestHandlerResult<R = unknown> =
102
>
| { readonly result: R; readonly error?: undefined }
103
>
| { readonly result?: undefined; readonly error: { readonly code: number; readonly message: string; readonly data?: unknown } };
104
>
105
>
/**
106
>
* Subset of `ChildProcessWithoutNullStreams` we actually use, so callers
107
>
* can pass either a real child process or an in-memory pair for tests.
108
>
*/
109
>
export interface ICodexAppServerTransport {
110
>
readonly stdin: Writable;
111
>
readonly stdout: Readable;
112
>
/** Force termination. Used as the 2 s grace force-kill fallback. */
113
>
kill(signal?: NodeJS.Signals): boolean;
114
>
/** Fires when the underlying process exits. */
115
>
readonly onExit: Event<{ readonly code: number | null; readonly signal: NodeJS.Signals | null }>;
116
>
/** Registers a one-shot exit listener that may outlive client disposal. */
117
>
onExitOnce(listener: (e: { readonly code: number | null; readonly signal: NodeJS.Signals | null }) => void): void;
118
>
}
119
>
120
>
/**
121
>
* Generic JSON-RPC client over a {@link ICodexAppServerTransport}.
122
>
*
123
>
* The client doesn't know anything about codex's domain — it just
124
>
* brokers typed requests and notifications in both directions. The
125
>
* `CodexAgent` layer above translates this into `IAgent` semantics.
126
>
*
127
>
* Lifecycle:
128
>
* - Construct with an active transport. The client immediately starts
129
>
* reading from `transport.stdout`.
130
>
* - Send requests / notifications via {@link request} / {@link notify}.
131
>
* - Register handlers for server-initiated traffic via
132
>
* {@link onNotification} / {@link onRequest}.
133
>
* - On `dispose()`: send EOF on stdin, wait up to 2 s for clean exit,
134
>
* then SIGKILL. Outstanding requests reject with `CancellationError`.
135
>
*/
136
>
export interface ICodexAppServerClient extends IDisposable {
137
>
/** Fires once when the transport exits (clean or otherwise). */
138
>
readonly onExit: Event<{ readonly code: number | null; readonly signal: NodeJS.Signals | null }>;
139
>
140
>
/** Fires when the underlying transport rejects further writes (process exited unexpectedly). */
141
>
readonly onTransportError: Event<Error>;
142
>
143
>
/**
144
>
* Issue a request. Resolves with the typed response payload, or
145
>
* rejects with {@link JsonRpcError} for protocol-level errors and
146
>
* {@link CancellationError} on dispose.
147
>
*/
148
>
request<M extends ClientRequestMethod, R = unknown>(
149
>
method: M,
150
>
params: ClientRequestParams<M>,
151
>
): Promise<R>;
152
>
153
>
/**
154
>
* Fire-and-forget notification. Does not throw if the transport
155
>
* already closed; lost notifications are surfaced via
156
>
* `onTransportError`.
157
>
*/
158
>
notify<M extends ClientNotificationMethod>(
159
>
method: M,
160
>
params: ClientNotificationParams<M>,
161
>
): void;
162
>
163
>
/**
164
>
* Register a handler for a server-pushed notification.
165
>
*
166
>
* Only one handler per method; subsequent registrations replace the
167
>
* previous handler.
168
>
*/
169
>
onNotification<M extends ServerNotificationMethod>(
170
>
method: M,
171
>
handler: (params: ServerNotificationParams<M>) => void,
172
>
): IDisposable;
173
>
174
>
/**
175
>
* Register a handler for a server-initiated request. The handler
176
>
* returns a typed result or an error envelope.
177
>
*
178
>
* Only one handler per method; subsequent registrations replace the
179
>
* previous handler. Unregistered methods reply with
180
>
* {@link JsonRpcErrorCode.MethodNotFound}.
181
>
*/
182
>
onRequest<M extends ServerRequestMethod, R = unknown>(
183
>
method: M,
184
>
handler: (params: ServerRequestParams<M>) => Promise<ServerRequestHandlerResult<R>> | ServerRequestHandlerResult<R>,
185
>
): IDisposable;
186
>
}
187
>
188
>
interface IPendingRequest {
189
>
resolve(value: unknown): void;
190
>
reject(reason: unknown): void;
191
>
readonly method: string;
192
>
}
193
>
194
>
const GRACE_KILL_MS = 2_000;
195
>
196
>
export class CodexAppServerClient extends Disposable implements ICodexAppServerClient {
197
>
198
>
private readonly _onExit = this._register(new Emitter<{ readonly code: number | null; readonly signal: NodeJS.Signals | null }>());
199
>
readonly onExit = this._onExit.event;
200
>
201
>
private readonly _onTransportError = this._register(new Emitter<Error>());
202
>
readonly onTransportError = this._onTransportError.event;
203
>
204
>
private _nextId = 1;
205
>
private readonly _pending = new Map<number, IPendingRequest>();
206
>
private readonly _notificationHandlers = new Map<string, (params: unknown) => void>();
207
>
private readonly _requestHandlers = new Map<string, (params: unknown) => Promise<ServerRequestHandlerResult<unknown>>>();
208
>
209
>
private _exited = false;
210
>
private _disposed = false;
211
>
private _buf = '';
212
>
213
>
constructor(
214
private readonly _transport: ICodexAppServerTransport,
215
private readonly _onLog?: (level: 'info' | 'warn' | 'error', message: string) => void,