@ai-sdk/harness-codex 1.0.125 → 1.0.128

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+
3
+ type JsonRpcId = number | string;
4
+
5
+ type JsonRpcError = {
6
+ code: number;
7
+ message: string;
8
+ data?: unknown;
9
+ };
10
+
11
+ export type CodexAppServerNotification = {
12
+ method: string;
13
+ params?: unknown;
14
+ };
15
+
16
+ export type CodexAppServerRequest = CodexAppServerNotification & {
17
+ id: JsonRpcId;
18
+ };
19
+
20
+ export class CodexAppServerRequestError extends Error {
21
+ readonly method: string;
22
+ readonly code: number;
23
+ readonly data: unknown;
24
+
25
+ constructor({ method, error }: { method: string; error: JsonRpcError }) {
26
+ super(`Codex app-server ${method} failed: ${error.message}`);
27
+ this.name = 'CodexAppServerRequestError';
28
+ this.method = method;
29
+ this.code = error.code;
30
+ this.data = error.data;
31
+ }
32
+ }
33
+
34
+ export class CodexAppServerClient {
35
+ private readonly child: ChildProcessWithoutNullStreams;
36
+ private readonly onNotification: (
37
+ notification: CodexAppServerNotification,
38
+ ) => void;
39
+ private readonly onRequest: (
40
+ request: CodexAppServerRequest,
41
+ ) => Promise<unknown>;
42
+ private readonly onStderr: (text: string) => void;
43
+ private readonly pending = new Map<
44
+ JsonRpcId,
45
+ {
46
+ method: string;
47
+ resolve: (value: unknown) => void;
48
+ reject: (error: unknown) => void;
49
+ }
50
+ >();
51
+ private nextRequestId = 1;
52
+ private stdoutBuffer = '';
53
+ private parseQueue = Promise.resolve();
54
+ private closed = false;
55
+ private failure: unknown;
56
+ private readonly failed: Promise<unknown>;
57
+ private resolveFailure: (error: unknown) => void = () => {};
58
+ private exitResult: {
59
+ code: number | null;
60
+ signal: NodeJS.Signals | null;
61
+ } | null = null;
62
+ private readonly exited: Promise<{
63
+ code: number | null;
64
+ signal: NodeJS.Signals | null;
65
+ }>;
66
+
67
+ constructor({
68
+ executable,
69
+ args,
70
+ cwd,
71
+ env,
72
+ onNotification,
73
+ onRequest,
74
+ onStderr,
75
+ }: {
76
+ executable: string;
77
+ args: string[];
78
+ cwd: string;
79
+ env: NodeJS.ProcessEnv;
80
+ onNotification: (notification: CodexAppServerNotification) => void;
81
+ onRequest: (request: CodexAppServerRequest) => Promise<unknown>;
82
+ onStderr: (text: string) => void;
83
+ }) {
84
+ this.onNotification = onNotification;
85
+ this.onRequest = onRequest;
86
+ this.onStderr = onStderr;
87
+ this.failed = new Promise(resolve => {
88
+ this.resolveFailure = resolve;
89
+ });
90
+ this.child = spawn(executable, args, {
91
+ cwd,
92
+ env,
93
+ stdio: ['pipe', 'pipe', 'pipe'],
94
+ });
95
+ this.exited = new Promise(resolve => {
96
+ this.child.once('exit', (code, signal) => {
97
+ this.exitResult = { code, signal };
98
+ this.fail(
99
+ new Error(
100
+ `Codex app-server exited (code ${code ?? 'null'}, signal ${signal ?? 'null'}).`,
101
+ ),
102
+ );
103
+ resolve(this.exitResult);
104
+ });
105
+ });
106
+ this.child.once('error', error => this.fail(error));
107
+ this.child.stdout.setEncoding('utf8');
108
+ this.child.stdout.on('data', chunk => this.consumeStdout(String(chunk)));
109
+ this.child.stderr.setEncoding('utf8');
110
+ this.child.stderr.on('data', chunk => this.onStderr(String(chunk)));
111
+ }
112
+
113
+ async initialize({
114
+ clientName,
115
+ clientVersion,
116
+ }: {
117
+ clientName: string;
118
+ clientVersion: string;
119
+ }): Promise<void> {
120
+ await this.request({
121
+ method: 'initialize',
122
+ params: {
123
+ clientInfo: { name: clientName, version: clientVersion },
124
+ capabilities: { experimentalApi: true },
125
+ },
126
+ });
127
+ this.notify({ method: 'initialized' });
128
+ }
129
+
130
+ request({
131
+ method,
132
+ params,
133
+ }: {
134
+ method: string;
135
+ params?: unknown;
136
+ }): Promise<unknown> {
137
+ if (this.closed || this.exitResult != null) {
138
+ return Promise.reject(new Error('Codex app-server is not running.'));
139
+ }
140
+ const id = this.nextRequestId++;
141
+ const response = new Promise<unknown>((resolve, reject) => {
142
+ this.pending.set(id, { method, resolve, reject });
143
+ });
144
+ try {
145
+ this.write({ id, method, ...(params === undefined ? {} : { params }) });
146
+ } catch (error) {
147
+ this.pending.delete(id);
148
+ return Promise.reject(error);
149
+ }
150
+ return response;
151
+ }
152
+
153
+ notify({ method, params }: { method: string; params?: unknown }): void {
154
+ this.write({ method, ...(params === undefined ? {} : { params }) });
155
+ }
156
+
157
+ async close(): Promise<void> {
158
+ if (this.closed) return;
159
+ this.closed = true;
160
+ this.child.stdin.end();
161
+ if (await this.waitForExit({ timeoutMs: 500 })) return;
162
+ this.child.kill('SIGTERM');
163
+ if (await this.waitForExit({ timeoutMs: 1_000 })) return;
164
+ this.child.kill('SIGKILL');
165
+ await this.waitForExit({ timeoutMs: 1_000 });
166
+ }
167
+
168
+ waitUntilExit(): Promise<{
169
+ code: number | null;
170
+ signal: NodeJS.Signals | null;
171
+ }> {
172
+ return this.exited;
173
+ }
174
+
175
+ waitUntilFailure(): Promise<never> {
176
+ return this.failed.then(error => {
177
+ throw error;
178
+ });
179
+ }
180
+
181
+ private write(message: Record<string, unknown>): void {
182
+ if (this.closed || this.exitResult != null || !this.child.stdin.writable) {
183
+ throw new Error('Codex app-server is not writable.');
184
+ }
185
+ this.child.stdin.write(`${JSON.stringify(message)}\n`);
186
+ }
187
+
188
+ private consumeStdout(chunk: string): void {
189
+ this.stdoutBuffer += chunk;
190
+ let newline = this.stdoutBuffer.indexOf('\n');
191
+ while (newline !== -1) {
192
+ const line = this.stdoutBuffer.slice(0, newline).trim();
193
+ this.stdoutBuffer = this.stdoutBuffer.slice(newline + 1);
194
+ if (line.length > 0) {
195
+ this.parseQueue = this.parseQueue
196
+ .then(() => this.processLine(line))
197
+ .catch(error => this.fail(error));
198
+ }
199
+ newline = this.stdoutBuffer.indexOf('\n');
200
+ }
201
+ }
202
+
203
+ private async processLine(line: string): Promise<void> {
204
+ /*
205
+ * Bridge code runs in an isolated runtime where provider-utils is not
206
+ * available. The app-server is the bridge's own child process, and every
207
+ * JSON-RPC field consumed below is checked before it is used.
208
+ */
209
+ let message: unknown;
210
+ try {
211
+ message = JSON.parse(line);
212
+ } catch {
213
+ throw new Error('Codex app-server wrote an invalid JSON-RPC message.');
214
+ }
215
+ if (!isRecord(message)) {
216
+ throw new Error('Codex app-server wrote an invalid JSON-RPC message.');
217
+ }
218
+ if (typeof message.method === 'string') {
219
+ if (isJsonRpcId(message.id)) {
220
+ void this.handleServerRequest({
221
+ id: message.id,
222
+ method: message.method,
223
+ ...(message.params === undefined ? {} : { params: message.params }),
224
+ });
225
+ } else {
226
+ this.onNotification({
227
+ method: message.method,
228
+ ...(message.params === undefined ? {} : { params: message.params }),
229
+ });
230
+ }
231
+ return;
232
+ }
233
+ if (!isJsonRpcId(message.id)) {
234
+ throw new Error('Codex app-server response is missing a valid id.');
235
+ }
236
+ const pending = this.pending.get(message.id);
237
+ if (pending == null) return;
238
+ this.pending.delete(message.id);
239
+ if (isJsonRpcError(message.error)) {
240
+ pending.reject(
241
+ new CodexAppServerRequestError({
242
+ method: pending.method,
243
+ error: message.error,
244
+ }),
245
+ );
246
+ return;
247
+ }
248
+ if (!Object.prototype.hasOwnProperty.call(message, 'result')) {
249
+ pending.reject(
250
+ new Error(`Codex app-server ${pending.method} response has no result.`),
251
+ );
252
+ return;
253
+ }
254
+ pending.resolve(message.result);
255
+ }
256
+
257
+ private async handleServerRequest(
258
+ request: CodexAppServerRequest,
259
+ ): Promise<void> {
260
+ try {
261
+ const result = await this.onRequest(request);
262
+ if (!this.closed && this.exitResult == null) {
263
+ this.write({ id: request.id, result });
264
+ }
265
+ } catch (error) {
266
+ if (!this.closed && this.exitResult == null) {
267
+ this.write({
268
+ id: request.id,
269
+ error: {
270
+ code: -32603,
271
+ message: error instanceof Error ? error.message : String(error),
272
+ },
273
+ });
274
+ }
275
+ }
276
+ }
277
+
278
+ private failPending(error: unknown): void {
279
+ for (const pending of this.pending.values()) pending.reject(error);
280
+ this.pending.clear();
281
+ }
282
+
283
+ private fail(error: unknown): void {
284
+ this.failPending(error);
285
+ if (this.failure !== undefined) return;
286
+ this.failure = error;
287
+ this.resolveFailure(error);
288
+ }
289
+
290
+ private async waitForExit({
291
+ timeoutMs,
292
+ }: {
293
+ timeoutMs: number;
294
+ }): Promise<boolean> {
295
+ if (this.exitResult != null) return true;
296
+ return Promise.race([
297
+ this.exited.then(() => true),
298
+ new Promise<boolean>(resolve => {
299
+ const timer = setTimeout(() => resolve(false), timeoutMs);
300
+ timer.unref?.();
301
+ }),
302
+ ]);
303
+ }
304
+ }
305
+
306
+ function isRecord(value: unknown): value is Record<string, unknown> {
307
+ return value != null && typeof value === 'object' && !Array.isArray(value);
308
+ }
309
+
310
+ function isJsonRpcId(value: unknown): value is JsonRpcId {
311
+ return typeof value === 'string' || typeof value === 'number';
312
+ }
313
+
314
+ function isJsonRpcError(value: unknown): value is JsonRpcError {
315
+ return (
316
+ isRecord(value) &&
317
+ typeof value.code === 'number' &&
318
+ typeof value.message === 'string'
319
+ );
320
+ }