@parall/codex-agent 1.26.2 → 1.27.0
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/dist/dispatch.d.ts +1 -0
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +38 -0
- package/dist/index.js +7 -1
- package/dist/jsonrpc-client.d.ts +2 -0
- package/dist/jsonrpc-client.d.ts.map +1 -1
- package/dist/jsonrpc-client.js +17 -1
- package/dist/workspace.d.ts +23 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +74 -0
- package/package.json +4 -4
- package/src/dispatch.ts +39 -0
- package/src/index.ts +7 -1
- package/src/jsonrpc-client.ts +18 -1
- package/src/workspace.ts +79 -0
package/dist/dispatch.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ type CodexAppServerAdapterOptions = Pick<CodexAgentConfig, "approvalPolicy" | "c
|
|
|
5
5
|
sessionManager: CodexSessionManager;
|
|
6
6
|
log?: GatewayLogger;
|
|
7
7
|
contextFilePath?: string;
|
|
8
|
+
useParallProvider?: boolean;
|
|
8
9
|
};
|
|
9
10
|
/**
|
|
10
11
|
* Bridge driver backed by `codex app-server --listen stdio://`.
|
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAOF;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,eAAe;IA6B/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA5BjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,IAAI,CAA+C;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAE/D,YAAY,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAK/E,QAAQ,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAwMxG,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMjD,WAAW,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAuBhG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKhB,IAAI;YAaI,aAAa;YAyBb,OAAO;IA8DrB,OAAO,CAAC,qBAAqB;YAiCf,UAAU;IA2CxB,OAAO,CAAC,iBAAiB;CA2B1B"}
|
package/dist/dispatch.js
CHANGED
|
@@ -163,6 +163,11 @@ export class CodexAppServerAdapter {
|
|
|
163
163
|
// resume normally instead of permanently losing conversation context.
|
|
164
164
|
// Fork sessions don't retry: agent-core spawns a fresh fork next trigger.
|
|
165
165
|
if (!isMainSession) {
|
|
166
|
+
yield {
|
|
167
|
+
type: "runtime_session",
|
|
168
|
+
runtimeSessionId: threadId,
|
|
169
|
+
runtimeLaneKey: sessionKey,
|
|
170
|
+
};
|
|
166
171
|
yield { type: "error", message: `Codex turn/start failed: ${message}` };
|
|
167
172
|
return;
|
|
168
173
|
}
|
|
@@ -173,6 +178,11 @@ export class CodexAppServerAdapter {
|
|
|
173
178
|
freshThreadId = await this.openThread(client, { resumeId: undefined });
|
|
174
179
|
}
|
|
175
180
|
catch (createErr) {
|
|
181
|
+
yield {
|
|
182
|
+
type: "runtime_session",
|
|
183
|
+
runtimeSessionId: threadId,
|
|
184
|
+
runtimeLaneKey: sessionKey,
|
|
185
|
+
};
|
|
176
186
|
yield { type: "error", message: `Codex turn/start failed; could not create replacement thread: ${errToString(createErr)}` };
|
|
177
187
|
return;
|
|
178
188
|
}
|
|
@@ -185,6 +195,11 @@ export class CodexAppServerAdapter {
|
|
|
185
195
|
// issue. Don't clobber the persisted threadId; next dispatch will try
|
|
186
196
|
// resume again.
|
|
187
197
|
this.activeTurns.delete(freshThreadId);
|
|
198
|
+
yield {
|
|
199
|
+
type: "runtime_session",
|
|
200
|
+
runtimeSessionId: threadId,
|
|
201
|
+
runtimeLaneKey: sessionKey,
|
|
202
|
+
};
|
|
188
203
|
yield { type: "error", message: `Codex turn/start failed after retry: ${errToString(retryErr)}` };
|
|
189
204
|
return;
|
|
190
205
|
}
|
|
@@ -196,6 +211,11 @@ export class CodexAppServerAdapter {
|
|
|
196
211
|
threadId = freshThreadId;
|
|
197
212
|
}
|
|
198
213
|
void turnStartResult; // turnId comes back here but isn't needed — we key by threadId
|
|
214
|
+
yield {
|
|
215
|
+
type: "runtime_session",
|
|
216
|
+
runtimeSessionId: threadId,
|
|
217
|
+
runtimeLaneKey: sessionKey,
|
|
218
|
+
};
|
|
199
219
|
while (true) {
|
|
200
220
|
const envelope = await sink.next();
|
|
201
221
|
if (envelope.kind === "turn_end") {
|
|
@@ -211,6 +231,10 @@ export class CodexAppServerAdapter {
|
|
|
211
231
|
yield runtimeEvent;
|
|
212
232
|
continue;
|
|
213
233
|
}
|
|
234
|
+
if (runtimeEvent.type === "runtime_session") {
|
|
235
|
+
yield runtimeEvent;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
214
238
|
if (runtimeEvent.type === "text") {
|
|
215
239
|
// Layer 0 symmetric output contract: Codex's plain text is never
|
|
216
240
|
// projected as a chat message. Outbound messages must come from the
|
|
@@ -283,6 +307,17 @@ export class CodexAppServerAdapter {
|
|
|
283
307
|
}
|
|
284
308
|
}
|
|
285
309
|
async ensureStarted(log) {
|
|
310
|
+
if (this.client?.isDisposed()) {
|
|
311
|
+
for (const activeSink of this.activeTurns.values()) {
|
|
312
|
+
activeSink.push({ kind: "error", message: "Codex app-server disposed; resetting adapter" });
|
|
313
|
+
activeSink.close();
|
|
314
|
+
}
|
|
315
|
+
this.activeTurns.clear();
|
|
316
|
+
this.client = null;
|
|
317
|
+
this.proc = null;
|
|
318
|
+
this.initialized = false;
|
|
319
|
+
this.resumedThreadIds.clear();
|
|
320
|
+
}
|
|
286
321
|
if (this.initialized && this.client)
|
|
287
322
|
return;
|
|
288
323
|
if (this.startPromise)
|
|
@@ -396,6 +431,9 @@ export class CodexAppServerAdapter {
|
|
|
396
431
|
const commonParams = {
|
|
397
432
|
approvalPolicy: normalizeApprovalPolicy(this.opts.approvalPolicy),
|
|
398
433
|
};
|
|
434
|
+
if (this.opts.useParallProvider) {
|
|
435
|
+
commonParams.modelProvider = "parall";
|
|
436
|
+
}
|
|
399
437
|
commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
|
|
400
438
|
if (this.opts.model)
|
|
401
439
|
commonParams.model = this.opts.model;
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
|
|
|
5
5
|
import { buildCodexRuntimeKey, contextFilePathForSession, resolveCodexAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from "./config.js";
|
|
6
6
|
import { CodexAppServerAdapter } from "./dispatch.js";
|
|
7
7
|
import { CodexSessionManager } from "./session-manager.js";
|
|
8
|
-
import { ensureCodexWorkspace, ensureWorkspaceTrusted } from "./workspace.js";
|
|
8
|
+
import { ensureCodexWorkspace, ensureParallProvider, ensureWorkspaceTrusted, isParallProxyMode } from "./workspace.js";
|
|
9
9
|
function createLogger(prefix) {
|
|
10
10
|
return {
|
|
11
11
|
info: (msg) => console.log(`[${prefix}] ${msg}`),
|
|
@@ -36,6 +36,11 @@ async function main() {
|
|
|
36
36
|
const me = await getAgentMeWithLegacyFallback(client, config.orgId);
|
|
37
37
|
const agentUserId = me.id;
|
|
38
38
|
ensureWorkspaceTrusted(config.codexHome, config.workspaceDir, log);
|
|
39
|
+
const useParallProvider = isParallProxyMode();
|
|
40
|
+
if (useParallProvider) {
|
|
41
|
+
ensureParallProvider(config.codexHome, config.apiUrl, log);
|
|
42
|
+
log.info("parall custom provider configured (Responses API HTTP/SSE mode)");
|
|
43
|
+
}
|
|
39
44
|
ensureCodexWorkspace(config.workspaceDir, log, {
|
|
40
45
|
userId: agentUserId,
|
|
41
46
|
displayName: me.display_name,
|
|
@@ -70,6 +75,7 @@ async function main() {
|
|
|
70
75
|
sessionManager,
|
|
71
76
|
log,
|
|
72
77
|
contextFilePath: mainContextFilePath,
|
|
78
|
+
useParallProvider,
|
|
73
79
|
});
|
|
74
80
|
const gateway = new ParallAgentGateway({
|
|
75
81
|
accountId: agentUserId,
|
package/dist/jsonrpc-client.d.ts
CHANGED
|
@@ -42,8 +42,10 @@ export declare class JsonRpcStdioClient {
|
|
|
42
42
|
setNotificationHandler(handler: NotificationHandler): void;
|
|
43
43
|
sendRequest(method: string, params?: unknown): Promise<unknown>;
|
|
44
44
|
sendNotification(method: string, params?: unknown): void;
|
|
45
|
+
isDisposed(): boolean;
|
|
45
46
|
dispose(err: Error): void;
|
|
46
47
|
private writeFrame;
|
|
48
|
+
private killUnhealthy;
|
|
47
49
|
private ingest;
|
|
48
50
|
private handleLine;
|
|
49
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonrpc-client.d.ts","sourceRoot":"","sources":["../src/jsonrpc-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAEzE,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjC,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC3D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;AAW5E;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAQ3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IARnC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,QAAQ,CAAS;gBAGN,IAAI,EAAE,8BAA8B,EACpC,gBAAgB,GAAE,MAAmC;
|
|
1
|
+
{"version":3,"file":"jsonrpc-client.d.ts","sourceRoot":"","sources":["../src/jsonrpc-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAEzE,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjC,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC3D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;AAW5E;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAQ3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IARnC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,QAAQ,CAAS;gBAGN,IAAI,EAAE,8BAA8B,EACpC,gBAAgB,GAAE,MAAmC;IASxE,sBAAsB,CAAC,OAAO,EAAE,mBAAmB;IAInD,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAoB/D,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO;IAMjD,UAAU,IAAI,OAAO;IAIrB,OAAO,CAAC,GAAG,EAAE,KAAK;IAUlB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,UAAU;CAyBnB"}
|
package/dist/jsonrpc-client.js
CHANGED
|
@@ -23,6 +23,7 @@ export class JsonRpcStdioClient {
|
|
|
23
23
|
proc.stdout.on("data", (chunk) => this.ingest(chunk));
|
|
24
24
|
proc.once("close", () => this.dispose(new Error("app-server subprocess closed")));
|
|
25
25
|
proc.once("error", (err) => this.dispose(err));
|
|
26
|
+
proc.stdin.on("error", (err) => this.killUnhealthy(err));
|
|
26
27
|
}
|
|
27
28
|
setNotificationHandler(handler) {
|
|
28
29
|
this.onNotification = handler;
|
|
@@ -40,6 +41,7 @@ export class JsonRpcStdioClient {
|
|
|
40
41
|
return;
|
|
41
42
|
this.pending.delete(id);
|
|
42
43
|
reject(new Error(`JSON-RPC request "${method}" timed out after ${this.requestTimeoutMs}ms`));
|
|
44
|
+
this.killUnhealthy(new Error(`request "${method}" timed out; subprocess assumed hung`));
|
|
43
45
|
}, this.requestTimeoutMs);
|
|
44
46
|
this.pending.set(id, { resolve, reject, timer });
|
|
45
47
|
});
|
|
@@ -52,6 +54,9 @@ export class JsonRpcStdioClient {
|
|
|
52
54
|
const notification = { jsonrpc: "2.0", method, params };
|
|
53
55
|
this.writeFrame(notification);
|
|
54
56
|
}
|
|
57
|
+
isDisposed() {
|
|
58
|
+
return this.disposed;
|
|
59
|
+
}
|
|
55
60
|
dispose(err) {
|
|
56
61
|
if (this.disposed)
|
|
57
62
|
return;
|
|
@@ -63,7 +68,18 @@ export class JsonRpcStdioClient {
|
|
|
63
68
|
this.pending.clear();
|
|
64
69
|
}
|
|
65
70
|
writeFrame(message) {
|
|
66
|
-
|
|
71
|
+
try {
|
|
72
|
+
this.proc.stdin.write(`${JSON.stringify(message)}\n`);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
this.killUnhealthy(err instanceof Error ? err : new Error(String(err)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
killUnhealthy(err) {
|
|
79
|
+
this.dispose(err);
|
|
80
|
+
if (this.proc.exitCode === null && this.proc.signalCode === null) {
|
|
81
|
+
this.proc.kill("SIGTERM");
|
|
82
|
+
}
|
|
67
83
|
}
|
|
68
84
|
ingest(chunk) {
|
|
69
85
|
this.buffer += chunk;
|
package/dist/workspace.d.ts
CHANGED
|
@@ -12,6 +12,29 @@ import type { AgentIdentity } from "@parall/agent-core";
|
|
|
12
12
|
export declare function ensureWorkspaceTrusted(codexHome: string, workspaceDir: string, log?: {
|
|
13
13
|
warn: (msg: string) => void;
|
|
14
14
|
}): void;
|
|
15
|
+
/**
|
|
16
|
+
* Returns true when the agent is using the Parall LLM proxy (llm_source=parall)
|
|
17
|
+
* rather than a BYO custom provider or runtime_auth. Detected by checking
|
|
18
|
+
* whether OPENAI_BASE_URL points to the Parall API.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isParallProxyMode(env?: NodeJS.ProcessEnv): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Ensure a custom `parall` model provider is configured in the global Codex
|
|
23
|
+
* config. Codex CLI's built-in `openai` provider hardcodes
|
|
24
|
+
* `wss://api.openai.com` for the Responses API, ignoring OPENAI_BASE_URL.
|
|
25
|
+
* A custom provider with `supports_websockets = false` forces HTTP/SSE
|
|
26
|
+
* transport that correctly routes through `base_url`.
|
|
27
|
+
*
|
|
28
|
+
* Only written when the agent uses the Parall proxy (llm_source=parall).
|
|
29
|
+
* BYO custom providers (llm_source=custom) use the built-in openai
|
|
30
|
+
* provider with their own real API key, which works with the hardcoded
|
|
31
|
+
* api.openai.com endpoint.
|
|
32
|
+
*
|
|
33
|
+
* Provider-managed: overwritten on every boot (env vars are the SSOT).
|
|
34
|
+
*/
|
|
35
|
+
export declare function ensureParallProvider(codexHome: string, apiUrl: string, log?: {
|
|
36
|
+
warn: (msg: string) => void;
|
|
37
|
+
}): void;
|
|
15
38
|
export declare function ensureCodexWorkspace(workspaceDir: string, log?: {
|
|
16
39
|
warn: (msg: string) => void;
|
|
17
40
|
}, agentIdentity?: AgentIdentity): void;
|
package/dist/workspace.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CAyEN;AASD,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACrC,aAAa,CAAC,EAAE,aAAa,GAC5B,IAAI,CAqBN"}
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CAyEN;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAK/E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CA+CN;AASD,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACrC,aAAa,CAAC,EAAE,aAAa,GAC5B,IAAI,CAqBN"}
|
package/dist/workspace.js
CHANGED
|
@@ -91,6 +91,80 @@ export function ensureWorkspaceTrusted(codexHome, workspaceDir, log) {
|
|
|
91
91
|
log?.warn(`failed to write Codex project trust for ${normalizedPath}: ${String(err)}`);
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Returns true when the agent is using the Parall LLM proxy (llm_source=parall)
|
|
96
|
+
* rather than a BYO custom provider or runtime_auth. Detected by checking
|
|
97
|
+
* whether OPENAI_BASE_URL points to the Parall API.
|
|
98
|
+
*/
|
|
99
|
+
export function isParallProxyMode(env = process.env) {
|
|
100
|
+
const baseUrl = env.OPENAI_BASE_URL?.trim()?.replace(/\/+$/, "");
|
|
101
|
+
const apiUrl = env.PRLL_API_URL?.trim()?.replace(/\/+$/, "");
|
|
102
|
+
if (!baseUrl || !apiUrl)
|
|
103
|
+
return false;
|
|
104
|
+
return baseUrl === apiUrl || baseUrl.startsWith(`${apiUrl}/`);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Ensure a custom `parall` model provider is configured in the global Codex
|
|
108
|
+
* config. Codex CLI's built-in `openai` provider hardcodes
|
|
109
|
+
* `wss://api.openai.com` for the Responses API, ignoring OPENAI_BASE_URL.
|
|
110
|
+
* A custom provider with `supports_websockets = false` forces HTTP/SSE
|
|
111
|
+
* transport that correctly routes through `base_url`.
|
|
112
|
+
*
|
|
113
|
+
* Only written when the agent uses the Parall proxy (llm_source=parall).
|
|
114
|
+
* BYO custom providers (llm_source=custom) use the built-in openai
|
|
115
|
+
* provider with their own real API key, which works with the hardcoded
|
|
116
|
+
* api.openai.com endpoint.
|
|
117
|
+
*
|
|
118
|
+
* Provider-managed: overwritten on every boot (env vars are the SSOT).
|
|
119
|
+
*/
|
|
120
|
+
export function ensureParallProvider(codexHome, apiUrl, log) {
|
|
121
|
+
const configPath = path.join(codexHome, "config.toml");
|
|
122
|
+
const baseUrl = apiUrl.replace(/\/$/, "") + "/api/llm/v1";
|
|
123
|
+
try {
|
|
124
|
+
let content = "";
|
|
125
|
+
try {
|
|
126
|
+
content = fs.readFileSync(configPath, "utf8");
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err.code !== "ENOENT")
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
const sectionHeader = "[model_providers.parall]";
|
|
133
|
+
const authHeader = "[model_providers.parall.auth]";
|
|
134
|
+
const providerBlock = [
|
|
135
|
+
sectionHeader,
|
|
136
|
+
'name = "Parall Proxy"',
|
|
137
|
+
`base_url = "${baseUrl}"`,
|
|
138
|
+
'wire_api = "responses"',
|
|
139
|
+
"supports_websockets = false",
|
|
140
|
+
"requires_openai_auth = false",
|
|
141
|
+
"",
|
|
142
|
+
authHeader,
|
|
143
|
+
'command = "printenv"',
|
|
144
|
+
'args = ["OPENAI_API_KEY"]',
|
|
145
|
+
].join("\n");
|
|
146
|
+
const headerIdx = content.indexOf(sectionHeader);
|
|
147
|
+
if (headerIdx !== -1) {
|
|
148
|
+
const sectionEnd = findSectionEnd(content, headerIdx + sectionHeader.length + 1);
|
|
149
|
+
// Only consume a contiguous [model_providers.parall.auth] subtable —
|
|
150
|
+
// one that sits within the provider's own section, not after unrelated
|
|
151
|
+
// tables that happen to appear between provider and auth.
|
|
152
|
+
const authIdx = content.indexOf(authHeader, headerIdx);
|
|
153
|
+
const blockEnd = authIdx !== -1 && authIdx < sectionEnd
|
|
154
|
+
? findSectionEnd(content, authIdx + authHeader.length + 1)
|
|
155
|
+
: sectionEnd;
|
|
156
|
+
content = content.substring(0, headerIdx) + providerBlock + content.substring(blockEnd);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
content = content.trimEnd() + "\n\n" + providerBlock + "\n";
|
|
160
|
+
}
|
|
161
|
+
fs.mkdirSync(codexHome, { recursive: true });
|
|
162
|
+
fs.writeFileSync(configPath, content, "utf8");
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
throw new Error(`failed to write Parall provider config: ${String(err)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
94
168
|
function findSectionEnd(content, fromIndex) {
|
|
95
169
|
// A TOML section ends at the next `[` that starts a new table header.
|
|
96
170
|
// Match `[` at the beginning of a line (not inside a value).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/codex-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "Codex CLI bridge runtime for self-hosted Parall agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"smol-toml": "^1.6.1",
|
|
29
|
-
"@parall/
|
|
30
|
-
"@parall/sdk": "1.
|
|
31
|
-
"@parall/
|
|
29
|
+
"@parall/agent-core": "1.27.0",
|
|
30
|
+
"@parall/sdk": "1.27.0",
|
|
31
|
+
"@parall/cli": "1.27.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^22.0.0",
|
package/src/dispatch.ts
CHANGED
|
@@ -36,6 +36,7 @@ type CodexAppServerAdapterOptions = Pick<
|
|
|
36
36
|
sessionManager: CodexSessionManager;
|
|
37
37
|
log?: GatewayLogger;
|
|
38
38
|
contextFilePath?: string;
|
|
39
|
+
useParallProvider?: boolean;
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
type TurnEventEnvelope =
|
|
@@ -200,6 +201,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
200
201
|
// resume normally instead of permanently losing conversation context.
|
|
201
202
|
// Fork sessions don't retry: agent-core spawns a fresh fork next trigger.
|
|
202
203
|
if (!isMainSession) {
|
|
204
|
+
yield {
|
|
205
|
+
type: "runtime_session",
|
|
206
|
+
runtimeSessionId: threadId,
|
|
207
|
+
runtimeLaneKey: sessionKey,
|
|
208
|
+
};
|
|
203
209
|
yield { type: "error", message: `Codex turn/start failed: ${message}` };
|
|
204
210
|
return;
|
|
205
211
|
}
|
|
@@ -209,6 +215,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
209
215
|
try {
|
|
210
216
|
freshThreadId = await this.openThread(client, { resumeId: undefined });
|
|
211
217
|
} catch (createErr) {
|
|
218
|
+
yield {
|
|
219
|
+
type: "runtime_session",
|
|
220
|
+
runtimeSessionId: threadId,
|
|
221
|
+
runtimeLaneKey: sessionKey,
|
|
222
|
+
};
|
|
212
223
|
yield { type: "error", message: `Codex turn/start failed; could not create replacement thread: ${errToString(createErr)}` };
|
|
213
224
|
return;
|
|
214
225
|
}
|
|
@@ -220,6 +231,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
220
231
|
// issue. Don't clobber the persisted threadId; next dispatch will try
|
|
221
232
|
// resume again.
|
|
222
233
|
this.activeTurns.delete(freshThreadId);
|
|
234
|
+
yield {
|
|
235
|
+
type: "runtime_session",
|
|
236
|
+
runtimeSessionId: threadId,
|
|
237
|
+
runtimeLaneKey: sessionKey,
|
|
238
|
+
};
|
|
223
239
|
yield { type: "error", message: `Codex turn/start failed after retry: ${errToString(retryErr)}` };
|
|
224
240
|
return;
|
|
225
241
|
}
|
|
@@ -232,6 +248,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
232
248
|
}
|
|
233
249
|
|
|
234
250
|
void turnStartResult; // turnId comes back here but isn't needed — we key by threadId
|
|
251
|
+
yield {
|
|
252
|
+
type: "runtime_session",
|
|
253
|
+
runtimeSessionId: threadId,
|
|
254
|
+
runtimeLaneKey: sessionKey,
|
|
255
|
+
};
|
|
235
256
|
|
|
236
257
|
while (true) {
|
|
237
258
|
const envelope = await sink.next();
|
|
@@ -248,6 +269,10 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
248
269
|
yield runtimeEvent;
|
|
249
270
|
continue;
|
|
250
271
|
}
|
|
272
|
+
if (runtimeEvent.type === "runtime_session") {
|
|
273
|
+
yield runtimeEvent;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
251
276
|
if (runtimeEvent.type === "text") {
|
|
252
277
|
// Layer 0 symmetric output contract: Codex's plain text is never
|
|
253
278
|
// projected as a chat message. Outbound messages must come from the
|
|
@@ -321,6 +346,17 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
321
346
|
}
|
|
322
347
|
|
|
323
348
|
private async ensureStarted(log?: GatewayLogger): Promise<void> {
|
|
349
|
+
if (this.client?.isDisposed()) {
|
|
350
|
+
for (const activeSink of this.activeTurns.values()) {
|
|
351
|
+
activeSink.push({ kind: "error", message: "Codex app-server disposed; resetting adapter" });
|
|
352
|
+
activeSink.close();
|
|
353
|
+
}
|
|
354
|
+
this.activeTurns.clear();
|
|
355
|
+
this.client = null;
|
|
356
|
+
this.proc = null;
|
|
357
|
+
this.initialized = false;
|
|
358
|
+
this.resumedThreadIds.clear();
|
|
359
|
+
}
|
|
324
360
|
if (this.initialized && this.client) return;
|
|
325
361
|
if (this.startPromise) return this.startPromise;
|
|
326
362
|
this.startPromise = this.doStart(log);
|
|
@@ -445,6 +481,9 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
445
481
|
const commonParams: Record<string, unknown> = {
|
|
446
482
|
approvalPolicy: normalizeApprovalPolicy(this.opts.approvalPolicy),
|
|
447
483
|
};
|
|
484
|
+
if (this.opts.useParallProvider) {
|
|
485
|
+
commonParams.modelProvider = "parall";
|
|
486
|
+
}
|
|
448
487
|
commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
|
|
449
488
|
if (this.opts.model) commonParams.model = this.opts.model;
|
|
450
489
|
if (this.opts.reasoningEffort) {
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./config.js";
|
|
14
14
|
import { CodexAppServerAdapter } from "./dispatch.js";
|
|
15
15
|
import { CodexSessionManager } from "./session-manager.js";
|
|
16
|
-
import { ensureCodexWorkspace, ensureWorkspaceTrusted } from "./workspace.js";
|
|
16
|
+
import { ensureCodexWorkspace, ensureParallProvider, ensureWorkspaceTrusted, isParallProxyMode } from "./workspace.js";
|
|
17
17
|
|
|
18
18
|
function createLogger(prefix: string) {
|
|
19
19
|
return {
|
|
@@ -48,6 +48,11 @@ async function main() {
|
|
|
48
48
|
const me = await getAgentMeWithLegacyFallback(client, config.orgId);
|
|
49
49
|
const agentUserId = me.id;
|
|
50
50
|
ensureWorkspaceTrusted(config.codexHome, config.workspaceDir, log);
|
|
51
|
+
const useParallProvider = isParallProxyMode();
|
|
52
|
+
if (useParallProvider) {
|
|
53
|
+
ensureParallProvider(config.codexHome, config.apiUrl, log);
|
|
54
|
+
log.info("parall custom provider configured (Responses API HTTP/SSE mode)");
|
|
55
|
+
}
|
|
51
56
|
ensureCodexWorkspace(config.workspaceDir, log, {
|
|
52
57
|
userId: agentUserId,
|
|
53
58
|
displayName: me.display_name,
|
|
@@ -87,6 +92,7 @@ async function main() {
|
|
|
87
92
|
sessionManager,
|
|
88
93
|
log,
|
|
89
94
|
contextFilePath: mainContextFilePath,
|
|
95
|
+
useParallProvider,
|
|
90
96
|
});
|
|
91
97
|
|
|
92
98
|
const gateway = new ParallAgentGateway({
|
package/src/jsonrpc-client.ts
CHANGED
|
@@ -56,6 +56,7 @@ export class JsonRpcStdioClient {
|
|
|
56
56
|
proc.stdout.on("data", (chunk: string) => this.ingest(chunk));
|
|
57
57
|
proc.once("close", () => this.dispose(new Error("app-server subprocess closed")));
|
|
58
58
|
proc.once("error", (err) => this.dispose(err));
|
|
59
|
+
proc.stdin.on("error", (err) => this.killUnhealthy(err));
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
setNotificationHandler(handler: NotificationHandler) {
|
|
@@ -74,6 +75,7 @@ export class JsonRpcStdioClient {
|
|
|
74
75
|
if (!pending) return;
|
|
75
76
|
this.pending.delete(id);
|
|
76
77
|
reject(new Error(`JSON-RPC request "${method}" timed out after ${this.requestTimeoutMs}ms`));
|
|
78
|
+
this.killUnhealthy(new Error(`request "${method}" timed out; subprocess assumed hung`));
|
|
77
79
|
}, this.requestTimeoutMs);
|
|
78
80
|
this.pending.set(id, { resolve, reject, timer });
|
|
79
81
|
});
|
|
@@ -87,6 +89,10 @@ export class JsonRpcStdioClient {
|
|
|
87
89
|
this.writeFrame(notification);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
isDisposed(): boolean {
|
|
93
|
+
return this.disposed;
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
dispose(err: Error) {
|
|
91
97
|
if (this.disposed) return;
|
|
92
98
|
this.disposed = true;
|
|
@@ -98,7 +104,18 @@ export class JsonRpcStdioClient {
|
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
private writeFrame(message: object) {
|
|
101
|
-
|
|
107
|
+
try {
|
|
108
|
+
this.proc.stdin.write(`${JSON.stringify(message)}\n`);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
this.killUnhealthy(err instanceof Error ? err : new Error(String(err)));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private killUnhealthy(err: Error) {
|
|
115
|
+
this.dispose(err);
|
|
116
|
+
if (this.proc.exitCode === null && this.proc.signalCode === null) {
|
|
117
|
+
this.proc.kill("SIGTERM");
|
|
118
|
+
}
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
private ingest(chunk: string) {
|
package/src/workspace.ts
CHANGED
|
@@ -100,6 +100,85 @@ export function ensureWorkspaceTrusted(
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Returns true when the agent is using the Parall LLM proxy (llm_source=parall)
|
|
105
|
+
* rather than a BYO custom provider or runtime_auth. Detected by checking
|
|
106
|
+
* whether OPENAI_BASE_URL points to the Parall API.
|
|
107
|
+
*/
|
|
108
|
+
export function isParallProxyMode(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
109
|
+
const baseUrl = env.OPENAI_BASE_URL?.trim()?.replace(/\/+$/, "");
|
|
110
|
+
const apiUrl = env.PRLL_API_URL?.trim()?.replace(/\/+$/, "");
|
|
111
|
+
if (!baseUrl || !apiUrl) return false;
|
|
112
|
+
return baseUrl === apiUrl || baseUrl.startsWith(`${apiUrl}/`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ensure a custom `parall` model provider is configured in the global Codex
|
|
117
|
+
* config. Codex CLI's built-in `openai` provider hardcodes
|
|
118
|
+
* `wss://api.openai.com` for the Responses API, ignoring OPENAI_BASE_URL.
|
|
119
|
+
* A custom provider with `supports_websockets = false` forces HTTP/SSE
|
|
120
|
+
* transport that correctly routes through `base_url`.
|
|
121
|
+
*
|
|
122
|
+
* Only written when the agent uses the Parall proxy (llm_source=parall).
|
|
123
|
+
* BYO custom providers (llm_source=custom) use the built-in openai
|
|
124
|
+
* provider with their own real API key, which works with the hardcoded
|
|
125
|
+
* api.openai.com endpoint.
|
|
126
|
+
*
|
|
127
|
+
* Provider-managed: overwritten on every boot (env vars are the SSOT).
|
|
128
|
+
*/
|
|
129
|
+
export function ensureParallProvider(
|
|
130
|
+
codexHome: string,
|
|
131
|
+
apiUrl: string,
|
|
132
|
+
log?: { warn: (msg: string) => void },
|
|
133
|
+
): void {
|
|
134
|
+
const configPath = path.join(codexHome, "config.toml");
|
|
135
|
+
const baseUrl = apiUrl.replace(/\/$/, "") + "/api/llm/v1";
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
let content = "";
|
|
139
|
+
try {
|
|
140
|
+
content = fs.readFileSync(configPath, "utf8");
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const sectionHeader = "[model_providers.parall]";
|
|
146
|
+
const authHeader = "[model_providers.parall.auth]";
|
|
147
|
+
const providerBlock = [
|
|
148
|
+
sectionHeader,
|
|
149
|
+
'name = "Parall Proxy"',
|
|
150
|
+
`base_url = "${baseUrl}"`,
|
|
151
|
+
'wire_api = "responses"',
|
|
152
|
+
"supports_websockets = false",
|
|
153
|
+
"requires_openai_auth = false",
|
|
154
|
+
"",
|
|
155
|
+
authHeader,
|
|
156
|
+
'command = "printenv"',
|
|
157
|
+
'args = ["OPENAI_API_KEY"]',
|
|
158
|
+
].join("\n");
|
|
159
|
+
|
|
160
|
+
const headerIdx = content.indexOf(sectionHeader);
|
|
161
|
+
if (headerIdx !== -1) {
|
|
162
|
+
const sectionEnd = findSectionEnd(content, headerIdx + sectionHeader.length + 1);
|
|
163
|
+
// Only consume a contiguous [model_providers.parall.auth] subtable —
|
|
164
|
+
// one that sits within the provider's own section, not after unrelated
|
|
165
|
+
// tables that happen to appear between provider and auth.
|
|
166
|
+
const authIdx = content.indexOf(authHeader, headerIdx);
|
|
167
|
+
const blockEnd = authIdx !== -1 && authIdx < sectionEnd
|
|
168
|
+
? findSectionEnd(content, authIdx + authHeader.length + 1)
|
|
169
|
+
: sectionEnd;
|
|
170
|
+
content = content.substring(0, headerIdx) + providerBlock + content.substring(blockEnd);
|
|
171
|
+
} else {
|
|
172
|
+
content = content.trimEnd() + "\n\n" + providerBlock + "\n";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fs.mkdirSync(codexHome, { recursive: true });
|
|
176
|
+
fs.writeFileSync(configPath, content, "utf8");
|
|
177
|
+
} catch (err) {
|
|
178
|
+
throw new Error(`failed to write Parall provider config: ${String(err)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
103
182
|
function findSectionEnd(content: string, fromIndex: number): number {
|
|
104
183
|
// A TOML section ends at the next `[` that starts a new table header.
|
|
105
184
|
// Match `[` at the beginning of a line (not inside a value).
|