@harapter/adapter-codex 0.1.1

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,1033 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { ExtensionRegistry, HarnessError, assertSessionCompatibility, assertSessionOwnership, providerSessionId, runId, } from '@harapter/core';
3
+ import { JsonRpcRemoteError, JsonRpcStdioTransport, JsonRpcTransportError, } from '@harapter/transport-jsonrpc-stdio';
4
+ import { CODEX_PROVIDER_ID, CODEX_SESSION_COMPATIBILITY_REF, codexCompatibilityIdentity, encodeCodexInteractionResponse, mapCodexNotification, mapCodexServerRequest, parseCodexInitializeResponse, parseCodexThreadResponse, parseCodexTurnStartResponse, prepareCodexInput, prepareCodexSessionParams, prepareCodexTurnParams, redactCodexEvent, } from './protocol.js';
5
+ const descriptor = {
6
+ providerId: CODEX_PROVIDER_ID,
7
+ displayName: 'Codex App Server',
8
+ connectionKinds: ['process'],
9
+ documentationUrl: 'https://developers.openai.com/codex/app-server',
10
+ };
11
+ const defaultMaxRunEvents = 128;
12
+ const defaultCancelSettlementTimeoutMs = 10_000;
13
+ const childTerminationTimeoutMs = 2_000;
14
+ const maximumTimerMilliseconds = 2_147_483_647;
15
+ /** Create a fresh Codex App Server Adapter factory. */
16
+ export function createCodexProviderFactory() {
17
+ return {
18
+ descriptor: () => ({
19
+ ...descriptor,
20
+ connectionKinds: [...descriptor.connectionKinds],
21
+ }),
22
+ connect: async (profile) => connectCodex(profile),
23
+ };
24
+ }
25
+ async function connectCodex(profile) {
26
+ validateProfile(profile);
27
+ const options = connectionOptions(profile.providerOptions);
28
+ let transport;
29
+ try {
30
+ transport = await spawnTransport(profile, options.transport);
31
+ }
32
+ catch (error) {
33
+ throw mapError(error, profile, 'spawn', true);
34
+ }
35
+ try {
36
+ const initialize = await transport.request('initialize', {
37
+ clientInfo: {
38
+ name: 'harapter',
39
+ title: 'Harapter',
40
+ version: '0.0.0',
41
+ },
42
+ capabilities: {
43
+ experimentalApi: false,
44
+ requestAttestation: false,
45
+ },
46
+ });
47
+ const runtime = parseCodexInitializeResponse(initialize);
48
+ await transport.notify('initialized');
49
+ return new CodexClient(snapshotProfile(profile), transport, runtime, options.cancelSettlementTimeoutMs, options.maxRunEvents);
50
+ }
51
+ catch (error) {
52
+ await transport.close().catch(() => undefined);
53
+ throw mapError(error, profile, 'initialize', true);
54
+ }
55
+ }
56
+ class CodexClient {
57
+ profile;
58
+ transport;
59
+ runtime;
60
+ cancelSettlementTimeoutMs;
61
+ maxRunEvents;
62
+ activeByThread = new Map();
63
+ activeByTurn = new Map();
64
+ ephemeralThreads = new Set();
65
+ extensionRegistry = new ExtensionRegistry(CODEX_PROVIDER_ID);
66
+ nativeClient;
67
+ pendingByLocalId = new Map();
68
+ pendingByWireKey = new Map();
69
+ seenTurnIds = new Set();
70
+ startingByThread = new Map();
71
+ unknownListeners = new Set();
72
+ closePromise;
73
+ closed = false;
74
+ interactionSerial = 0;
75
+ runSerial = 0;
76
+ constructor(profile, transport, runtime, cancelSettlementTimeoutMs, maxRunEvents) {
77
+ this.profile = profile;
78
+ this.transport = transport;
79
+ this.runtime = runtime;
80
+ this.cancelSettlementTimeoutMs = cancelSettlementTimeoutMs;
81
+ this.maxRunEvents = maxRunEvents;
82
+ this.nativeClient = Object.freeze({
83
+ runtimeIdentity: this.runtimeIdentity(),
84
+ request: (method, params, options) => this.nativeRequest(method, params, options),
85
+ notify: (method, params) => this.nativeNotify(method, params),
86
+ onUnknownEvent: (listener) => {
87
+ this.unknownListeners.add(listener);
88
+ return () => {
89
+ this.unknownListeners.delete(listener);
90
+ };
91
+ },
92
+ });
93
+ void this.pump().catch(() => undefined);
94
+ }
95
+ descriptor() {
96
+ return Promise.resolve({
97
+ providerId: CODEX_PROVIDER_ID,
98
+ profileId: this.profile.profileId,
99
+ displayName: this.profile.displayName,
100
+ connectionKind: 'process',
101
+ runtime: {
102
+ name: 'Codex App Server',
103
+ version: this.runtime.runtimeVersion,
104
+ protocol: 'JSONL RPC',
105
+ protocolVersion: 'stable',
106
+ },
107
+ compatibility: 'supported',
108
+ });
109
+ }
110
+ capabilities() {
111
+ return Promise.resolve(codexCapabilities(this.profile, this.runtimeIdentity()));
112
+ }
113
+ async createSession(input = {}) {
114
+ this.assertOpen();
115
+ const params = prepareCodexSessionParams(input);
116
+ try {
117
+ const response = await this.transport.request('thread/start', params);
118
+ const threadId = parseCodexThreadResponse(response);
119
+ if (params['ephemeral'] === true)
120
+ this.ephemeralThreads.add(threadId);
121
+ return new CodexSession(this, providerSessionId(threadId), params['ephemeral'] !== true);
122
+ }
123
+ catch (error) {
124
+ throw mapError(error, this.profile, 'thread/start', false, this.transport);
125
+ }
126
+ }
127
+ async resumeSession(ref) {
128
+ this.assertOpen();
129
+ assertSessionOwnership(ref, CODEX_PROVIDER_ID, this.profile.profileId);
130
+ if (this.ephemeralThreads.has(ref.providerSessionId) ||
131
+ isEphemeralSessionRef(ref)) {
132
+ throw new HarnessError('unsupported_capability', 'An ephemeral Codex Thread cannot be resumed.', {
133
+ retryable: false,
134
+ providerId: CODEX_PROVIDER_ID,
135
+ profileId: this.profile.profileId,
136
+ details: { capability: 'session.resume' },
137
+ });
138
+ }
139
+ assertSessionCompatibility(ref, CODEX_SESSION_COMPATIBILITY_REF);
140
+ try {
141
+ const response = await this.transport.request('thread/resume', {
142
+ threadId: ref.providerSessionId,
143
+ });
144
+ const threadId = parseCodexThreadResponse(response);
145
+ if (threadId !== ref.providerSessionId) {
146
+ throw new HarnessError('session_provider_mismatch', 'Codex resumed a different Thread than requested.', {
147
+ retryable: false,
148
+ providerId: CODEX_PROVIDER_ID,
149
+ profileId: this.profile.profileId,
150
+ });
151
+ }
152
+ return new CodexSession(this, providerSessionId(threadId), true);
153
+ }
154
+ catch (error) {
155
+ throw mapError(error, this.profile, 'thread/resume', false, this.transport);
156
+ }
157
+ }
158
+ extensions() {
159
+ return this.extensionRegistry;
160
+ }
161
+ native(guard) {
162
+ const value = this.nativeClient;
163
+ return guard !== undefined && !guard(value) ? undefined : value;
164
+ }
165
+ close() {
166
+ this.closePromise ??= this.closeOnce();
167
+ return this.closePromise;
168
+ }
169
+ sessionRef(sessionId, resumable) {
170
+ return {
171
+ providerId: CODEX_PROVIDER_ID,
172
+ profileId: this.profile.profileId,
173
+ providerSessionId: sessionId,
174
+ compatibilityRef: CODEX_SESSION_COMPATIBILITY_REF,
175
+ providerState: {
176
+ createdRuntimeVersion: this.runtime.runtimeVersion,
177
+ ...(resumable ? {} : { ephemeral: true }),
178
+ },
179
+ };
180
+ }
181
+ capabilityManifest(resumable = true) {
182
+ return codexCapabilities(this.profile, this.runtimeIdentity(), resumable);
183
+ }
184
+ hasActiveRun(threadId) {
185
+ return (this.activeByThread.has(threadId) || this.startingByThread.has(threadId));
186
+ }
187
+ async startRun(sessionId, input, options = {}) {
188
+ this.assertOpen();
189
+ if (this.hasActiveRun(sessionId)) {
190
+ throw new HarnessError('run_conflict', 'Codex Thread already has an active Turn.', {
191
+ retryable: false,
192
+ providerId: CODEX_PROVIDER_ID,
193
+ profileId: this.profile.profileId,
194
+ });
195
+ }
196
+ const nativeInput = prepareCodexInput(input);
197
+ const overrides = prepareCodexTurnParams(options);
198
+ const starting = createStartingRun();
199
+ this.startingByThread.set(sessionId, starting);
200
+ try {
201
+ const response = await this.transport.request('turn/start', {
202
+ threadId: sessionId,
203
+ input: nativeInput,
204
+ ...overrides,
205
+ });
206
+ const turnId = parseCodexTurnStartResponse(response);
207
+ if (this.seenTurnIds.has(turnId)) {
208
+ this.abortConnection();
209
+ throw new HarnessError('provider_api_incompatible', 'Codex reused a Turn identifier on one connection.', {
210
+ retryable: false,
211
+ providerId: CODEX_PROVIDER_ID,
212
+ profileId: this.profile.profileId,
213
+ });
214
+ }
215
+ this.seenTurnIds.add(turnId);
216
+ const run = new CodexRun({
217
+ providerId: CODEX_PROVIDER_ID,
218
+ profileId: this.profile.profileId,
219
+ sessionId,
220
+ runId: runId(`codex-run-${String(++this.runSerial)}`),
221
+ providerRunId: turnId,
222
+ }, turnId, this.maxRunEvents, this.cancelSettlementTimeoutMs, options.timeoutMs, (activeRun) => this.interrupt(activeRun), () => {
223
+ this.abortConnection();
224
+ }, (activeRun) => {
225
+ this.onRunTerminal(activeRun);
226
+ });
227
+ this.activeByThread.set(sessionId, run);
228
+ this.activeByTurn.set(turnId, run);
229
+ starting.resolve(run);
230
+ return run;
231
+ }
232
+ catch (error) {
233
+ starting.resolve(undefined);
234
+ throw mapError(error, this.profile, 'turn/start', false, this.transport);
235
+ }
236
+ finally {
237
+ if (this.startingByThread.get(sessionId) === starting) {
238
+ this.startingByThread.delete(sessionId);
239
+ }
240
+ }
241
+ }
242
+ async respond(sessionId, requestId, response) {
243
+ this.assertOpen();
244
+ const pending = this.pendingByLocalId.get(requestId);
245
+ if (pending?.run.ref().sessionId !== sessionId ||
246
+ pending.run.isTerminal()) {
247
+ throw new HarnessError('invalid_request', 'The Codex interaction is no longer pending for this Session.', {
248
+ retryable: false,
249
+ providerId: CODEX_PROVIDER_ID,
250
+ profileId: this.profile.profileId,
251
+ });
252
+ }
253
+ const result = encodeCodexInteractionResponse(pending.request, response);
254
+ try {
255
+ await this.transport.respond(pending.wireId, result);
256
+ this.settlePending(pending, 'host');
257
+ }
258
+ catch (error) {
259
+ throw mapError(error, this.profile, 'interaction/respond', false, this.transport);
260
+ }
261
+ }
262
+ async pump() {
263
+ try {
264
+ for await (const message of this.transport.incoming()) {
265
+ if (message.kind === 'request')
266
+ await this.handleRequest(message);
267
+ else
268
+ await this.handleNotification(message.method, message.params);
269
+ }
270
+ }
271
+ catch {
272
+ if (!this.closed)
273
+ this.abortConnection();
274
+ }
275
+ }
276
+ async handleRequest(message) {
277
+ const requestId = `codex-interaction-${String(++this.interactionSerial)}`;
278
+ const mapped = mapCodexServerRequest(message.method, message.params, requestId);
279
+ const run = await this.routeWhenStarted(mapped.threadId, mapped.turnId);
280
+ if (run === undefined) {
281
+ this.emitUnknown(redactCodexEvent(message.method, message.params));
282
+ await this.transport.respondError(message.id, {
283
+ code: -32_601,
284
+ message: 'Harapter has no active Run for this request.',
285
+ });
286
+ return;
287
+ }
288
+ const pending = {
289
+ wireId: message.id,
290
+ wireKey: wireKey(message.id),
291
+ request: mapped,
292
+ run,
293
+ };
294
+ this.pendingByLocalId.set(mapped.interaction.requestId, pending);
295
+ this.pendingByWireKey.set(pending.wireKey, pending);
296
+ run.emit({ type: 'interaction.requested', data: mapped.interaction });
297
+ }
298
+ async handleNotification(method, params) {
299
+ if (method === 'serverRequest/resolved') {
300
+ const requestId = requestIdFromResolved(params);
301
+ const pending = requestId === undefined
302
+ ? undefined
303
+ : this.pendingByWireKey.get(wireKey(requestId));
304
+ if (pending !== undefined) {
305
+ this.settlePending(pending, 'provider', true);
306
+ }
307
+ else {
308
+ this.emitUnknown(redactCodexEvent(method, params));
309
+ }
310
+ return;
311
+ }
312
+ const mapping = mapCodexNotification(method, params);
313
+ const run = await this.routeWhenStarted(mapping.threadId, mapping.turnId);
314
+ if (run === undefined) {
315
+ let observed = false;
316
+ for (const event of mapping.events) {
317
+ if (event.raw === undefined)
318
+ continue;
319
+ observed = true;
320
+ this.emitUnknown(event.raw);
321
+ }
322
+ if (!observed && mapping.turnId !== undefined) {
323
+ this.emitUnknown(redactCodexEvent(method, params));
324
+ }
325
+ return;
326
+ }
327
+ if (method === 'turn/started')
328
+ run.confirmProviderStart();
329
+ for (const event of mapping.events) {
330
+ if (event.raw !== undefined)
331
+ this.emitUnknown(event.raw);
332
+ }
333
+ if (mapping.events.some(({ terminalResult }) => terminalResult !== undefined)) {
334
+ this.resolvePendingForRun(run, 'turn_terminal');
335
+ }
336
+ for (const event of mapping.events)
337
+ run.emit(event);
338
+ }
339
+ route(threadId, turnId) {
340
+ if (turnId === undefined)
341
+ return undefined;
342
+ const byTurn = this.activeByTurn.get(turnId);
343
+ if (byTurn !== undefined &&
344
+ (threadId === undefined || byTurn.ref().sessionId === threadId)) {
345
+ return byTurn;
346
+ }
347
+ return undefined;
348
+ }
349
+ async routeWhenStarted(threadId, turnId) {
350
+ const active = this.route(threadId, turnId);
351
+ if (active !== undefined || threadId === undefined)
352
+ return active;
353
+ const starting = this.startingByThread.get(threadId);
354
+ if (starting === undefined)
355
+ return undefined;
356
+ await starting.promise;
357
+ return this.route(threadId, turnId);
358
+ }
359
+ async interrupt(run) {
360
+ this.assertOpen();
361
+ try {
362
+ await this.transport.request('turn/interrupt', {
363
+ threadId: run.ref().sessionId,
364
+ turnId: run.turnId,
365
+ });
366
+ }
367
+ catch (error) {
368
+ throw mapError(error, this.profile, 'turn/interrupt', false, this.transport);
369
+ }
370
+ }
371
+ onRunTerminal(run) {
372
+ const reference = run.ref();
373
+ if (this.activeByThread.get(reference.sessionId) === run) {
374
+ this.activeByThread.delete(reference.sessionId);
375
+ }
376
+ if (this.activeByTurn.get(run.turnId) === run) {
377
+ this.activeByTurn.delete(run.turnId);
378
+ }
379
+ this.resolvePendingForRun(run, 'turn_terminal');
380
+ }
381
+ resolvePendingForRun(run, resolution) {
382
+ for (const pending of [...this.pendingByLocalId.values()]) {
383
+ if (pending.run !== run)
384
+ continue;
385
+ this.settlePending(pending, resolution, true);
386
+ }
387
+ }
388
+ settlePending(pending, resolution, abandonWire = false) {
389
+ if (this.pendingByLocalId.get(pending.request.interaction.requestId) !==
390
+ pending) {
391
+ return;
392
+ }
393
+ if (abandonWire)
394
+ this.transport.abandonInboundRequest(pending.wireId);
395
+ this.removePending(pending);
396
+ pending.run.emit({
397
+ type: 'interaction.resolved',
398
+ data: {
399
+ requestId: pending.request.interaction.requestId,
400
+ resolution,
401
+ },
402
+ });
403
+ }
404
+ removePending(pending) {
405
+ this.pendingByLocalId.delete(pending.request.interaction.requestId);
406
+ this.pendingByWireKey.delete(pending.wireKey);
407
+ }
408
+ emitUnknown(event) {
409
+ for (const listener of [...this.unknownListeners]) {
410
+ try {
411
+ listener(structuredClone(event));
412
+ }
413
+ catch {
414
+ // Native observers cannot break Provider lifecycle processing.
415
+ }
416
+ }
417
+ }
418
+ nativeRequest(method, params, options = {}) {
419
+ this.assertOpen();
420
+ return this.transport.request(method, params, options);
421
+ }
422
+ nativeNotify(method, params) {
423
+ this.assertOpen();
424
+ return this.transport.notify(method, params);
425
+ }
426
+ abortConnection() {
427
+ if (this.closed)
428
+ return;
429
+ this.closed = true;
430
+ for (const run of [...this.activeByTurn.values()]) {
431
+ this.resolvePendingForRun(run, 'connection_aborted');
432
+ run.abortConnection();
433
+ }
434
+ this.pendingByLocalId.clear();
435
+ this.pendingByWireKey.clear();
436
+ void this.transport.close().catch(() => undefined);
437
+ }
438
+ async closeOnce() {
439
+ if (!this.closed) {
440
+ this.closed = true;
441
+ for (const run of [...this.activeByTurn.values()]) {
442
+ this.resolvePendingForRun(run, 'connection_aborted');
443
+ run.abortConnection();
444
+ }
445
+ this.pendingByLocalId.clear();
446
+ this.pendingByWireKey.clear();
447
+ }
448
+ try {
449
+ await this.transport.close();
450
+ }
451
+ catch (error) {
452
+ throw mapError(error, this.profile, 'close', false, this.transport);
453
+ }
454
+ }
455
+ runtimeIdentity() {
456
+ return codexCompatibilityIdentity(this.runtime.runtimeVersion);
457
+ }
458
+ assertOpen() {
459
+ if (this.closed) {
460
+ throw new HarnessError('connection_aborted', 'Codex App Server is closed.', {
461
+ retryable: false,
462
+ providerId: CODEX_PROVIDER_ID,
463
+ profileId: this.profile.profileId,
464
+ });
465
+ }
466
+ }
467
+ }
468
+ class CodexSession {
469
+ client;
470
+ sessionId;
471
+ resumable;
472
+ closed = false;
473
+ constructor(client, sessionId, resumable) {
474
+ this.client = client;
475
+ this.sessionId = sessionId;
476
+ this.resumable = resumable;
477
+ }
478
+ ref() {
479
+ return this.client.sessionRef(this.sessionId, this.resumable);
480
+ }
481
+ capabilities() {
482
+ return Promise.resolve(this.client.capabilityManifest(this.resumable));
483
+ }
484
+ start(input, options) {
485
+ this.assertOpen();
486
+ return this.client.startRun(this.sessionId, input, options);
487
+ }
488
+ respond(requestId, response) {
489
+ this.assertOpen();
490
+ return this.client.respond(this.sessionId, requestId, response);
491
+ }
492
+ close() {
493
+ if (this.closed)
494
+ return Promise.resolve();
495
+ if (this.client.hasActiveRun(this.sessionId)) {
496
+ return Promise.reject(new HarnessError('run_conflict', 'Cannot close a Codex Session with an active Run.', {
497
+ retryable: false,
498
+ providerId: CODEX_PROVIDER_ID,
499
+ profileId: this.ref().profileId,
500
+ }));
501
+ }
502
+ this.closed = true;
503
+ return Promise.resolve();
504
+ }
505
+ assertOpen() {
506
+ if (this.closed) {
507
+ throw new HarnessError('session_not_found', 'Codex Session is closed.', {
508
+ retryable: false,
509
+ providerId: CODEX_PROVIDER_ID,
510
+ profileId: this.ref().profileId,
511
+ });
512
+ }
513
+ }
514
+ }
515
+ class CodexRun {
516
+ reference;
517
+ turnId;
518
+ cancelSettlementTimeoutMs;
519
+ interrupt;
520
+ abortOwnerConnection;
521
+ onTerminal;
522
+ eventQueue;
523
+ providerStart;
524
+ settlement;
525
+ timeout;
526
+ cancelPromise;
527
+ finalMessage;
528
+ finalResult;
529
+ resolveProviderStart;
530
+ resolveSettlement;
531
+ sequence = 0;
532
+ timeoutTriggered = false;
533
+ usage;
534
+ constructor(reference, turnId, maxRunEvents, cancelSettlementTimeoutMs, timeoutMs, interrupt, abortOwnerConnection, onTerminal) {
535
+ this.reference = reference;
536
+ this.turnId = turnId;
537
+ this.cancelSettlementTimeoutMs = cancelSettlementTimeoutMs;
538
+ this.interrupt = interrupt;
539
+ this.abortOwnerConnection = abortOwnerConnection;
540
+ this.onTerminal = onTerminal;
541
+ this.eventQueue = new EventQueue(maxRunEvents);
542
+ this.providerStart = new Promise((resolve) => {
543
+ this.resolveProviderStart = resolve;
544
+ });
545
+ this.settlement = new Promise((resolve) => {
546
+ this.resolveSettlement = resolve;
547
+ });
548
+ this.timeout =
549
+ timeoutMs === undefined
550
+ ? undefined
551
+ : setTimeout(() => {
552
+ this.timeoutTriggered = true;
553
+ void this.cancel().catch(() => {
554
+ this.abortOwnerConnection();
555
+ });
556
+ }, timeoutMs);
557
+ this.timeout?.unref();
558
+ this.emit({ type: 'run.started', data: {} });
559
+ }
560
+ ref() {
561
+ return { ...this.reference };
562
+ }
563
+ events() {
564
+ return this.eventQueue.iterable();
565
+ }
566
+ cancel() {
567
+ if (this.isTerminal())
568
+ return Promise.resolve({ mode: 'already_terminal' });
569
+ this.cancelPromise ??= this.cancelOnce();
570
+ return this.cancelPromise;
571
+ }
572
+ result() {
573
+ return this.settlement;
574
+ }
575
+ isTerminal() {
576
+ return this.finalResult !== undefined;
577
+ }
578
+ confirmProviderStart() {
579
+ if (!this.isTerminal())
580
+ this.resolveProviderStart(true);
581
+ }
582
+ emit(mapped) {
583
+ if (this.isTerminal())
584
+ return;
585
+ if (mapped.finalMessage !== undefined)
586
+ this.finalMessage = mapped.finalMessage;
587
+ if (mapped.usage !== undefined)
588
+ this.usage = mapped.usage;
589
+ if (mapped.terminalResult !== undefined) {
590
+ const result = {
591
+ ...mapped.terminalResult,
592
+ ...(mapped.terminalResult.status === 'completed' &&
593
+ this.finalMessage !== undefined
594
+ ? { finalMessage: this.finalMessage }
595
+ : {}),
596
+ ...(this.usage === undefined ? {} : { usage: this.usage }),
597
+ ...(this.timeoutTriggered &&
598
+ mapped.terminalResult.status === 'cancelled'
599
+ ? { providerResult: { reason: 'timeout' } }
600
+ : {}),
601
+ };
602
+ this.finish(result, mapped.type, {
603
+ ...mapped,
604
+ data: result,
605
+ });
606
+ return;
607
+ }
608
+ const event = this.portableEvent(mapped);
609
+ if (!this.eventQueue.push(event))
610
+ this.abortOwnerConnection();
611
+ }
612
+ abortConnection() {
613
+ if (this.isTerminal())
614
+ return;
615
+ const result = { status: 'connection_aborted' };
616
+ this.finish(result, 'connection.aborted', {
617
+ type: 'connection.aborted',
618
+ data: result,
619
+ });
620
+ }
621
+ async cancelOnce() {
622
+ const watchdog = setTimeout(() => {
623
+ this.abortOwnerConnection();
624
+ }, this.cancelSettlementTimeoutMs);
625
+ watchdog.unref();
626
+ try {
627
+ const providerStarted = await this.providerStart;
628
+ let interruptAcknowledged = false;
629
+ if (providerStarted && !this.isTerminal()) {
630
+ try {
631
+ await this.interrupt(this);
632
+ }
633
+ catch (error) {
634
+ if (this.finalResult?.status === 'connection_aborted')
635
+ return { mode: 'connection_aborted' };
636
+ throw error;
637
+ }
638
+ interruptAcknowledged = true;
639
+ }
640
+ const result = await this.settlement;
641
+ if (result.status === 'cancelled' && interruptAcknowledged)
642
+ return { mode: 'native' };
643
+ return result.status === 'connection_aborted'
644
+ ? { mode: 'connection_aborted' }
645
+ : { mode: 'already_terminal' };
646
+ }
647
+ finally {
648
+ clearTimeout(watchdog);
649
+ }
650
+ }
651
+ finish(result, terminalType, mapped) {
652
+ if (this.isTerminal())
653
+ return;
654
+ this.finalResult = result;
655
+ this.resolveProviderStart(false);
656
+ if (this.timeout !== undefined)
657
+ clearTimeout(this.timeout);
658
+ this.eventQueue.pushTerminal(this.portableEvent({ ...mapped, type: terminalType, data: result }));
659
+ this.eventQueue.close();
660
+ this.onTerminal(this);
661
+ this.resolveSettlement(result);
662
+ }
663
+ portableEvent(mapped) {
664
+ const sequence = this.sequence++;
665
+ return {
666
+ id: `${this.reference.runId}:event:${String(sequence)}`,
667
+ type: mapped.type,
668
+ providerId: this.reference.providerId,
669
+ profileId: this.reference.profileId,
670
+ sessionId: this.reference.sessionId,
671
+ runId: this.reference.runId,
672
+ sequence,
673
+ timestamp: new Date().toISOString(),
674
+ data: mapped.data,
675
+ ...(mapped.providerEventType === undefined
676
+ ? {}
677
+ : { providerEventType: mapped.providerEventType }),
678
+ ...(mapped.raw === undefined ? {} : { raw: mapped.raw }),
679
+ };
680
+ }
681
+ }
682
+ class EventQueue {
683
+ capacity;
684
+ values = [];
685
+ closed = false;
686
+ consumed = false;
687
+ waiter;
688
+ constructor(capacity) {
689
+ this.capacity = capacity;
690
+ }
691
+ push(event) {
692
+ if (this.closed)
693
+ return false;
694
+ if (this.waiter !== undefined) {
695
+ const waiter = this.waiter;
696
+ this.waiter = undefined;
697
+ waiter({ done: false, value: event });
698
+ return true;
699
+ }
700
+ if (this.values.length >= this.capacity - 1)
701
+ return false;
702
+ this.values.push(event);
703
+ return true;
704
+ }
705
+ pushTerminal(event) {
706
+ if (this.waiter !== undefined) {
707
+ const waiter = this.waiter;
708
+ this.waiter = undefined;
709
+ waiter({ done: false, value: event });
710
+ return;
711
+ }
712
+ this.values.push(event);
713
+ }
714
+ close() {
715
+ this.closed = true;
716
+ }
717
+ iterable() {
718
+ if (this.consumed) {
719
+ return {
720
+ [Symbol.asyncIterator]: () => ({
721
+ next: () => Promise.reject(new HarnessError('run_conflict', 'Codex Run events already have a consumer.', { retryable: false, providerId: CODEX_PROVIDER_ID })),
722
+ }),
723
+ };
724
+ }
725
+ this.consumed = true;
726
+ return {
727
+ [Symbol.asyncIterator]: () => ({
728
+ next: () => this.next(),
729
+ }),
730
+ };
731
+ }
732
+ next() {
733
+ const event = this.values.shift();
734
+ if (event !== undefined)
735
+ return Promise.resolve({ done: false, value: event });
736
+ if (this.closed)
737
+ return Promise.resolve({ done: true, value: undefined });
738
+ if (this.waiter !== undefined) {
739
+ return Promise.reject(new HarnessError('run_conflict', 'A Codex event read is already pending.', {
740
+ retryable: false,
741
+ providerId: CODEX_PROVIDER_ID,
742
+ }));
743
+ }
744
+ return new Promise((resolve) => {
745
+ this.waiter = resolve;
746
+ });
747
+ }
748
+ }
749
+ async function spawnTransport(profile, options) {
750
+ if (profile.connection.kind !== 'process')
751
+ throw profileInvalid(profile);
752
+ const child = spawn(profile.connection.command, [...(profile.connection.args ?? [])], {
753
+ cwd: profile.connection.cwd,
754
+ shell: false,
755
+ stdio: ['pipe', 'pipe', 'ignore'],
756
+ });
757
+ await processStarted(child);
758
+ try {
759
+ return new JsonRpcStdioTransport({
760
+ ...options,
761
+ readable: child.stdout,
762
+ writable: child.stdin,
763
+ cleanup: () => terminateChild(child),
764
+ });
765
+ }
766
+ catch (error) {
767
+ await terminateChild(child);
768
+ throw error;
769
+ }
770
+ }
771
+ function processStarted(child) {
772
+ return new Promise((resolve, reject) => {
773
+ const onSpawn = () => {
774
+ child.off('error', onError);
775
+ child.on('error', () => undefined);
776
+ resolve();
777
+ };
778
+ const onError = (error) => {
779
+ child.off('spawn', onSpawn);
780
+ reject(error);
781
+ };
782
+ child.once('spawn', onSpawn);
783
+ child.once('error', onError);
784
+ });
785
+ }
786
+ async function terminateChild(child) {
787
+ if (child.exitCode !== null || child.signalCode !== null)
788
+ return;
789
+ child.kill();
790
+ if (await waitForChildExit(child, childTerminationTimeoutMs))
791
+ return;
792
+ child.kill('SIGKILL');
793
+ if (await waitForChildExit(child, childTerminationTimeoutMs))
794
+ return;
795
+ throw new Error('Codex child process did not exit after forced termination.');
796
+ }
797
+ function waitForChildExit(child, timeoutMs) {
798
+ if (child.exitCode !== null || child.signalCode !== null) {
799
+ return Promise.resolve(true);
800
+ }
801
+ return new Promise((resolve) => {
802
+ let settled = false;
803
+ const onExit = () => {
804
+ finish(true);
805
+ };
806
+ const timer = setTimeout(() => {
807
+ finish(false);
808
+ }, timeoutMs);
809
+ function finish(exited) {
810
+ if (settled)
811
+ return;
812
+ settled = true;
813
+ clearTimeout(timer);
814
+ child.off('exit', onExit);
815
+ resolve(exited);
816
+ }
817
+ child.once('exit', onExit);
818
+ if (child.exitCode !== null || child.signalCode !== null)
819
+ finish(true);
820
+ });
821
+ }
822
+ function codexCapabilities(profile, runtimeIdentity, resumable = true) {
823
+ const native = { mode: 'native', source: 'schema' };
824
+ const unsupported = {
825
+ mode: 'unsupported',
826
+ source: 'schema',
827
+ };
828
+ const sessionResume = resumable
829
+ ? native
830
+ : {
831
+ mode: 'unsupported',
832
+ reason: 'Ephemeral Codex Threads are not persisted for resume.',
833
+ source: 'configuration',
834
+ };
835
+ return {
836
+ providerId: CODEX_PROVIDER_ID,
837
+ profileId: profile.profileId,
838
+ capabilities: {
839
+ 'session.create': native,
840
+ 'session.resume': sessionResume,
841
+ 'session.fork': unsupported,
842
+ 'session.close': { mode: 'adapter_controlled', source: 'configuration' },
843
+ 'run.stream': native,
844
+ 'run.cancel': native,
845
+ 'connection.abort': {
846
+ mode: 'adapter_controlled',
847
+ source: 'configuration',
848
+ },
849
+ 'input.text': native,
850
+ 'input.image': native,
851
+ 'input.file': unsupported,
852
+ 'interaction.approval': native,
853
+ 'interaction.user_input': {
854
+ mode: 'unsupported',
855
+ reason: 'Codex user-input requests require the experimental API.',
856
+ source: 'schema',
857
+ },
858
+ 'interaction.provider': native,
859
+ 'event.raw': { mode: 'adapter_controlled', source: 'configuration' },
860
+ 'native.client': native,
861
+ },
862
+ observedAt: new Date().toISOString(),
863
+ runtimeIdentity,
864
+ };
865
+ }
866
+ function connectionOptions(value) {
867
+ const options = value ?? {};
868
+ const allowed = new Set([
869
+ 'cancelSettlementTimeoutMs',
870
+ 'maxBufferedMessages',
871
+ 'maxMessageBytes',
872
+ 'maxPendingInboundRequests',
873
+ 'maxPendingRequests',
874
+ 'maxPendingWrites',
875
+ 'maxRunEvents',
876
+ 'requestTimeoutMs',
877
+ ]);
878
+ const unknown = Object.keys(options).find((key) => !allowed.has(key));
879
+ if (unknown !== undefined) {
880
+ throw new HarnessError('profile_invalid', 'Unsupported Codex Profile option.', { retryable: false, providerId: CODEX_PROVIDER_ID });
881
+ }
882
+ const transport = {};
883
+ for (const name of [
884
+ 'maxBufferedMessages',
885
+ 'maxMessageBytes',
886
+ 'maxPendingInboundRequests',
887
+ 'maxPendingRequests',
888
+ 'maxPendingWrites',
889
+ ]) {
890
+ if (options[name] !== undefined) {
891
+ transport[name] = positiveProfileInteger(options[name], name);
892
+ }
893
+ }
894
+ if (options['requestTimeoutMs'] !== undefined) {
895
+ transport['requestTimeoutMs'] = positiveProfileTimer(options['requestTimeoutMs'], 'requestTimeoutMs');
896
+ }
897
+ return {
898
+ cancelSettlementTimeoutMs: options['cancelSettlementTimeoutMs'] === undefined
899
+ ? defaultCancelSettlementTimeoutMs
900
+ : positiveProfileTimer(options['cancelSettlementTimeoutMs'], 'cancelSettlementTimeoutMs'),
901
+ maxRunEvents: runEventCapacity(options['maxRunEvents']),
902
+ transport,
903
+ };
904
+ }
905
+ function runEventCapacity(value) {
906
+ if (value === undefined)
907
+ return defaultMaxRunEvents;
908
+ const capacity = positiveProfileInteger(value, 'maxRunEvents');
909
+ if (capacity < 2) {
910
+ throw new HarnessError('profile_invalid', 'Codex maxRunEvents must reserve space for a terminal event.', { retryable: false, providerId: CODEX_PROVIDER_ID });
911
+ }
912
+ return capacity;
913
+ }
914
+ function validateProfile(profile) {
915
+ if (profile.providerId !== CODEX_PROVIDER_ID ||
916
+ profile.connection.kind !== 'process' ||
917
+ profile.connection.ownership !== 'adapter' ||
918
+ profile.connection.command.length === 0 ||
919
+ profile.connection.envRefs !== undefined) {
920
+ throw profileInvalid(profile);
921
+ }
922
+ }
923
+ function profileInvalid(profile) {
924
+ return new HarnessError('profile_invalid', 'Codex requires an adapter-owned process Profile without unresolved Secret references.', {
925
+ retryable: false,
926
+ providerId: CODEX_PROVIDER_ID,
927
+ profileId: profile.profileId,
928
+ });
929
+ }
930
+ function positiveProfileInteger(value, label) {
931
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
932
+ throw new HarnessError('profile_invalid', `Codex ${label} must be a positive integer.`, {
933
+ retryable: false,
934
+ providerId: CODEX_PROVIDER_ID,
935
+ });
936
+ }
937
+ return value;
938
+ }
939
+ function positiveProfileTimer(value, label) {
940
+ const timeout = positiveProfileInteger(value, label);
941
+ if (timeout > maximumTimerMilliseconds) {
942
+ throw new HarnessError('profile_invalid', `Codex ${label} exceeds the supported timer range.`, {
943
+ retryable: false,
944
+ providerId: CODEX_PROVIDER_ID,
945
+ });
946
+ }
947
+ return timeout;
948
+ }
949
+ function snapshotProfile(profile) {
950
+ return {
951
+ ...profile,
952
+ connection: {
953
+ ...profile.connection,
954
+ ...(profile.connection.kind === 'process' && profile.connection.args
955
+ ? { args: [...profile.connection.args] }
956
+ : {}),
957
+ },
958
+ ...(profile.providerOptions === undefined
959
+ ? {}
960
+ : { providerOptions: { ...profile.providerOptions } }),
961
+ };
962
+ }
963
+ function requestIdFromResolved(params) {
964
+ if (typeof params !== 'object' || params === null || Array.isArray(params)) {
965
+ return undefined;
966
+ }
967
+ const value = params['requestId'];
968
+ return typeof value === 'string' || typeof value === 'number'
969
+ ? value
970
+ : undefined;
971
+ }
972
+ function isEphemeralSessionRef(ref) {
973
+ return (typeof ref.providerState === 'object' &&
974
+ ref.providerState !== null &&
975
+ !Array.isArray(ref.providerState) &&
976
+ ref.providerState['ephemeral'] === true);
977
+ }
978
+ function wireKey(id) {
979
+ return `${typeof id}:${String(id)}`;
980
+ }
981
+ function createStartingRun() {
982
+ let resolve;
983
+ const promise = new Promise((settle) => {
984
+ resolve = settle;
985
+ });
986
+ return { promise, resolve };
987
+ }
988
+ function mapError(error, profile, phase, connecting = false, transport) {
989
+ if (error instanceof HarnessError)
990
+ return error;
991
+ if (error instanceof JsonRpcRemoteError) {
992
+ const remote = error.getRemoteError();
993
+ const code = remote.code === -32_601 ? 'provider_api_incompatible' : 'provider_error';
994
+ return new HarnessError(code, `Codex App Server rejected ${phase}.`, {
995
+ retryable: false,
996
+ providerId: CODEX_PROVIDER_ID,
997
+ profileId: profile.profileId,
998
+ providerCode: String(remote.code),
999
+ });
1000
+ }
1001
+ if (error instanceof JsonRpcTransportError) {
1002
+ const code = error.code === 'request_timeout'
1003
+ ? 'timeout'
1004
+ : connecting
1005
+ ? 'connection_failed'
1006
+ : transport?.isOpen() === false
1007
+ ? 'connection_aborted'
1008
+ : 'provider_error';
1009
+ return new HarnessError(code, `Codex App Server ${phase} did not complete.`, {
1010
+ retryable: error.code === 'request_timeout' ||
1011
+ error.code === 'capacity_exceeded',
1012
+ providerId: CODEX_PROVIDER_ID,
1013
+ profileId: profile.profileId,
1014
+ providerCode: error.code,
1015
+ });
1016
+ }
1017
+ const systemCode = typeof error === 'object' && error !== null && 'code' in error
1018
+ ? error.code
1019
+ : undefined;
1020
+ if (connecting && systemCode === 'ENOENT') {
1021
+ return new HarnessError('runtime_not_found', 'The configured Codex runtime was not found.', {
1022
+ retryable: false,
1023
+ providerId: CODEX_PROVIDER_ID,
1024
+ profileId: profile.profileId,
1025
+ });
1026
+ }
1027
+ return new HarnessError(connecting ? 'connection_failed' : 'provider_error', `Codex App Server ${phase} failed.`, {
1028
+ retryable: false,
1029
+ providerId: CODEX_PROVIDER_ID,
1030
+ profileId: profile.profileId,
1031
+ });
1032
+ }
1033
+ //# sourceMappingURL=adapter.js.map