@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.
- package/LICENSE +201 -0
- package/README.md +250 -0
- package/dist/adapter.d.ts +28 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +1033 -0
- package/dist/adapter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +77 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +631 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +53 -0
package/dist/adapter.js
ADDED
|
@@ -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
|