@love-moon/ai-sdk 0.2.38 → 0.2.40
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/client.d.ts +4 -1
- package/dist/client.js +14 -2
- package/dist/external-provider-registry.js +38 -2
- package/dist/providers/claude-agent-sdk-session.d.ts +4 -2
- package/dist/providers/claude-agent-sdk-session.js +25 -5
- package/dist/providers/codex-app-server-session.d.ts +3 -2
- package/dist/providers/codex-app-server-session.js +22 -4
- package/dist/providers/codex-exec-session.d.ts +116 -0
- package/dist/providers/codex-exec-session.js +583 -0
- package/dist/providers/copilot-sdk-session.d.ts +193 -0
- package/dist/providers/copilot-sdk-session.js +1463 -0
- package/dist/providers/kimi-cli-session.d.ts +3 -2
- package/dist/providers/kimi-cli-session.js +17 -3
- package/dist/providers/kimi-print-session.d.ts +125 -0
- package/dist/providers/kimi-print-session.js +633 -0
- package/dist/providers/opencode-sdk-session.d.ts +3 -2
- package/dist/providers/opencode-sdk-session.js +7 -2
- package/dist/session-factory.d.ts +7 -1
- package/dist/session-factory.js +48 -6
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +39 -20
- package/dist/transports/codex-app-server-transport.d.ts +1 -0
- package/dist/transports/codex-app-server-transport.js +10 -5
- package/dist/worker.js +39 -32
- package/package.json +3 -2
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { EventEmitter } from "node:events";
|
|
7
|
+
import readline from "node:readline";
|
|
8
|
+
import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, } from "../shared.js";
|
|
9
|
+
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
10
|
+
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
11
|
+
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
12
|
+
const CODEX_EXEC_PROVIDER_VARIANT = "codex-exec";
|
|
13
|
+
const DEFAULT_CODEX_EXEC_COMMAND = "codex";
|
|
14
|
+
function createTurnError(message, extras = {}) {
|
|
15
|
+
const error = new Error(message);
|
|
16
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
17
|
+
error[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
function buildEmptyTurnResult() {
|
|
22
|
+
return {
|
|
23
|
+
text: "",
|
|
24
|
+
usage: null,
|
|
25
|
+
items: [],
|
|
26
|
+
events: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function isCodexBackend(backend) {
|
|
30
|
+
const normalized = String(backend || "").trim().toLowerCase();
|
|
31
|
+
return normalized === "codex" || normalized === "code";
|
|
32
|
+
}
|
|
33
|
+
function normalizeCodexBackend(backend) {
|
|
34
|
+
return isCodexBackend(backend) ? "codex" : String(backend || "codex").trim().toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
function normalizeJsonSchema(jsonSchema) {
|
|
37
|
+
if (!jsonSchema) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (typeof jsonSchema === "string") {
|
|
41
|
+
return JSON.parse(jsonSchema);
|
|
42
|
+
}
|
|
43
|
+
if (typeof jsonSchema === "object") {
|
|
44
|
+
return jsonSchema;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function buildHistoryPrompt(history, promptText) {
|
|
49
|
+
const normalizedPrompt = typeof promptText === "string" ? promptText.trim() : "";
|
|
50
|
+
const historyText = Array.isArray(history)
|
|
51
|
+
? history
|
|
52
|
+
.map((item) => {
|
|
53
|
+
const role = String(item?.role || "").toLowerCase() === "assistant" ? "Assistant" : "User";
|
|
54
|
+
const content = String(item?.content || "").trim();
|
|
55
|
+
return content ? `${role}: ${content}` : "";
|
|
56
|
+
})
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join("\n\n")
|
|
59
|
+
: "";
|
|
60
|
+
if (historyText && normalizedPrompt) {
|
|
61
|
+
return [
|
|
62
|
+
"Continue the existing conversation with this history.",
|
|
63
|
+
"",
|
|
64
|
+
historyText,
|
|
65
|
+
"",
|
|
66
|
+
`User: ${normalizedPrompt}`,
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
|
69
|
+
if (historyText) {
|
|
70
|
+
return [
|
|
71
|
+
"Continue the existing conversation with this history.",
|
|
72
|
+
"",
|
|
73
|
+
historyText,
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
return normalizedPrompt;
|
|
77
|
+
}
|
|
78
|
+
function resolveExecPhase(event) {
|
|
79
|
+
const type = String(event?.type || "").trim().toLowerCase();
|
|
80
|
+
const itemType = String(event?.item?.type || "").trim().toLowerCase();
|
|
81
|
+
if (type.includes("reason") || itemType.includes("reason")) {
|
|
82
|
+
return "reasoning";
|
|
83
|
+
}
|
|
84
|
+
if (itemType.includes("command") ||
|
|
85
|
+
itemType.includes("tool") ||
|
|
86
|
+
itemType.includes("patch") ||
|
|
87
|
+
type.includes("command") ||
|
|
88
|
+
type.includes("tool")) {
|
|
89
|
+
return "command_execution";
|
|
90
|
+
}
|
|
91
|
+
if (itemType.includes("agent_message") || type.includes("message")) {
|
|
92
|
+
return "message_aggregation";
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
function statusLineForPhase(phase) {
|
|
97
|
+
switch (phase) {
|
|
98
|
+
case "reasoning":
|
|
99
|
+
return "codex is thinking";
|
|
100
|
+
case "command_execution":
|
|
101
|
+
return "codex is running tools";
|
|
102
|
+
case "message_aggregation":
|
|
103
|
+
return "codex is composing reply";
|
|
104
|
+
case "turn_started":
|
|
105
|
+
return "codex is working";
|
|
106
|
+
case "turn_completed":
|
|
107
|
+
return "codex finished";
|
|
108
|
+
case "turn_failed":
|
|
109
|
+
return "codex failed";
|
|
110
|
+
default:
|
|
111
|
+
return "codex is working";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function filterCodexExecBaseArgs(args) {
|
|
115
|
+
const filtered = [];
|
|
116
|
+
let skipNext = false;
|
|
117
|
+
let afterAppServer = false;
|
|
118
|
+
for (const rawArg of Array.isArray(args) ? args : []) {
|
|
119
|
+
const arg = String(rawArg || "");
|
|
120
|
+
if (!arg) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (skipNext) {
|
|
124
|
+
skipNext = false;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (arg === "app-server") {
|
|
128
|
+
afterAppServer = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (afterAppServer && arg === "--listen") {
|
|
132
|
+
skipNext = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (arg === "exec") {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
filtered.push(arg);
|
|
139
|
+
}
|
|
140
|
+
return filtered;
|
|
141
|
+
}
|
|
142
|
+
function readTextFileIfExists(filePath) {
|
|
143
|
+
try {
|
|
144
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
return fs.readFileSync(filePath, "utf8");
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function buildCodexExecEnv({ cwd, extraEnv = {}, ignoreCodexApiKey = false } = {}) {
|
|
154
|
+
const env = {
|
|
155
|
+
...process.env,
|
|
156
|
+
PWD: cwd,
|
|
157
|
+
...(extraEnv && typeof extraEnv === "object" ? extraEnv : {}),
|
|
158
|
+
};
|
|
159
|
+
if (ignoreCodexApiKey) {
|
|
160
|
+
delete env.CODEX_API_KEY;
|
|
161
|
+
}
|
|
162
|
+
return env;
|
|
163
|
+
}
|
|
164
|
+
export class CodexExecSession extends EventEmitter {
|
|
165
|
+
constructor(backend, options = {}) {
|
|
166
|
+
super();
|
|
167
|
+
this.backend = normalizeCodexBackend(backend);
|
|
168
|
+
this.options = options;
|
|
169
|
+
this.logger = normalizeLogger(options.logger);
|
|
170
|
+
this.cwd =
|
|
171
|
+
typeof options.cwd === "string" && options.cwd.trim()
|
|
172
|
+
? options.cwd.trim()
|
|
173
|
+
: process.cwd();
|
|
174
|
+
this.resumeSessionId = typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
|
|
175
|
+
this.sessionId = this.resumeSessionId || `codex-exec-${randomUUID()}`;
|
|
176
|
+
this.sessionInfo = {
|
|
177
|
+
backend: this.backend,
|
|
178
|
+
sessionId: this.sessionId,
|
|
179
|
+
model: typeof options.model === "string" && options.model.trim()
|
|
180
|
+
? options.model.trim()
|
|
181
|
+
: this.backend,
|
|
182
|
+
};
|
|
183
|
+
this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
|
|
184
|
+
this.closeRequested = false;
|
|
185
|
+
this.closed = false;
|
|
186
|
+
this.ignoreCodexApiKey = options.ignoreCodexApiKey === true;
|
|
187
|
+
this.currentTurn = null;
|
|
188
|
+
this.currentTurnStatus = null;
|
|
189
|
+
this.sessionAnnounced = false;
|
|
190
|
+
this.sessionMessageHandler = null;
|
|
191
|
+
this.workingStatusHandler = null;
|
|
192
|
+
this.activeReplyTarget = "";
|
|
193
|
+
this.lastReplyTarget = "";
|
|
194
|
+
this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
|
|
195
|
+
const envConfig = loadEnvConfig(options.configFile);
|
|
196
|
+
const proxyEnv = proxyToEnv(envConfig);
|
|
197
|
+
this.env = {
|
|
198
|
+
...(envConfig && typeof envConfig === "object" ? envConfig : {}),
|
|
199
|
+
...proxyEnv,
|
|
200
|
+
...(options.env && typeof options.env === "object" ? options.env : {}),
|
|
201
|
+
};
|
|
202
|
+
const commandLine = process.env.CONDUCTOR_CODEX_EXEC_COMMAND ||
|
|
203
|
+
options.commandLine ||
|
|
204
|
+
process.env.CONDUCTOR_CLI_COMMAND ||
|
|
205
|
+
DEFAULT_CODEX_EXEC_COMMAND;
|
|
206
|
+
const { command, args } = parseCommandParts(commandLine);
|
|
207
|
+
if (!command) {
|
|
208
|
+
throw new Error("Invalid codex exec command");
|
|
209
|
+
}
|
|
210
|
+
this.command = command;
|
|
211
|
+
this.baseArgs = filterCodexExecBaseArgs(args);
|
|
212
|
+
}
|
|
213
|
+
writeLog(message) {
|
|
214
|
+
emitLog(this.logger, message);
|
|
215
|
+
}
|
|
216
|
+
trace(message) {
|
|
217
|
+
this.writeLog(`[${this.backend}] [codex-exec] ${message}`);
|
|
218
|
+
}
|
|
219
|
+
get threadId() {
|
|
220
|
+
return this.sessionId;
|
|
221
|
+
}
|
|
222
|
+
get threadOptions() {
|
|
223
|
+
return {
|
|
224
|
+
model: this.sessionInfo?.model ||
|
|
225
|
+
(typeof this.options.model === "string" && this.options.model.trim()
|
|
226
|
+
? this.options.model.trim()
|
|
227
|
+
: this.backend),
|
|
228
|
+
modelProvider: this.sessionInfo?.modelProvider || undefined,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
getSnapshot() {
|
|
232
|
+
return {
|
|
233
|
+
backend: this.backend,
|
|
234
|
+
provider: CODEX_EXEC_PROVIDER_VARIANT,
|
|
235
|
+
cwd: this.cwd,
|
|
236
|
+
sessionId: this.sessionId || undefined,
|
|
237
|
+
sessionInfo: this.getSessionInfo(),
|
|
238
|
+
useSessionFileReplyStream: this.usesSessionFileReplyStream(),
|
|
239
|
+
resumeReady: false,
|
|
240
|
+
manualResume: null,
|
|
241
|
+
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
242
|
+
pid: this.currentTurn?.child?.pid || undefined,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
getSessionInfo() {
|
|
246
|
+
return this.sessionInfo ? { ...this.sessionInfo } : null;
|
|
247
|
+
}
|
|
248
|
+
getCurrentTurnStatus() {
|
|
249
|
+
return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
|
|
250
|
+
}
|
|
251
|
+
async ensureSessionInfo() {
|
|
252
|
+
this.announceSession();
|
|
253
|
+
return this.getSessionInfo();
|
|
254
|
+
}
|
|
255
|
+
async getSessionUsageSummary() {
|
|
256
|
+
return {
|
|
257
|
+
sessionId: this.sessionId || undefined,
|
|
258
|
+
sessionFilePath: undefined,
|
|
259
|
+
tokenUsagePercent: undefined,
|
|
260
|
+
contextUsagePercent: undefined,
|
|
261
|
+
tokenUsage: null,
|
|
262
|
+
rateLimits: null,
|
|
263
|
+
manualResume: null,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
usesSessionFileReplyStream() {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
setSessionMessageHandler(handler) {
|
|
270
|
+
this.sessionMessageHandler = typeof handler === "function" ? handler : null;
|
|
271
|
+
}
|
|
272
|
+
setWorkingStatusHandler(handler) {
|
|
273
|
+
this.workingStatusHandler = typeof handler === "function" ? handler : null;
|
|
274
|
+
}
|
|
275
|
+
setSessionReplyTarget(replyTo) {
|
|
276
|
+
const normalizedReplyTo = typeof replyTo === "string" ? replyTo.trim() : "";
|
|
277
|
+
this.activeReplyTarget = normalizedReplyTo;
|
|
278
|
+
if (normalizedReplyTo) {
|
|
279
|
+
this.lastReplyTarget = normalizedReplyTo;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
getCurrentReplyTarget() {
|
|
283
|
+
return this.activeReplyTarget || this.lastReplyTarget || undefined;
|
|
284
|
+
}
|
|
285
|
+
announceSession() {
|
|
286
|
+
if (this.sessionAnnounced) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.sessionAnnounced = true;
|
|
290
|
+
this.emit("session", this.getSessionInfo());
|
|
291
|
+
}
|
|
292
|
+
createSessionClosedError() {
|
|
293
|
+
return createTurnError("Codex exec session closed", {
|
|
294
|
+
reason: "session_closed",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
updateCurrentTurnStatus(payload) {
|
|
298
|
+
const updatedAtMs = Date.now();
|
|
299
|
+
this.currentTurnStatus = {
|
|
300
|
+
source: CODEX_EXEC_PROVIDER_VARIANT,
|
|
301
|
+
replyTo: this.getCurrentReplyTarget(),
|
|
302
|
+
thread_id: this.sessionId || undefined,
|
|
303
|
+
session_id: this.sessionId || undefined,
|
|
304
|
+
...payload,
|
|
305
|
+
updated_at: new Date(updatedAtMs).toISOString(),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async emitWorkingStatus(payload, onProgress = null) {
|
|
309
|
+
this.updateCurrentTurnStatus(payload);
|
|
310
|
+
const normalized = this.getCurrentTurnStatus();
|
|
311
|
+
if (typeof onProgress === "function") {
|
|
312
|
+
await onProgress(normalized);
|
|
313
|
+
}
|
|
314
|
+
if (typeof this.workingStatusHandler === "function") {
|
|
315
|
+
await this.workingStatusHandler(normalized);
|
|
316
|
+
}
|
|
317
|
+
this.emit("working_status", normalized);
|
|
318
|
+
return normalized;
|
|
319
|
+
}
|
|
320
|
+
async emitAssistantMessage(text) {
|
|
321
|
+
const normalizedText = typeof text === "string" ? text : "";
|
|
322
|
+
if (!normalizedText) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const payload = {
|
|
326
|
+
text: normalizedText,
|
|
327
|
+
preserveWhitespace: true,
|
|
328
|
+
replyTo: this.getCurrentReplyTarget(),
|
|
329
|
+
sessionId: this.sessionId || undefined,
|
|
330
|
+
backend: this.backend,
|
|
331
|
+
provider: CODEX_EXEC_PROVIDER_VARIANT,
|
|
332
|
+
};
|
|
333
|
+
if (typeof this.sessionMessageHandler === "function") {
|
|
334
|
+
await this.sessionMessageHandler(payload);
|
|
335
|
+
}
|
|
336
|
+
this.emit("assistant_message", payload);
|
|
337
|
+
}
|
|
338
|
+
buildPrompt(promptText) {
|
|
339
|
+
return buildHistoryPrompt(this.history, promptText);
|
|
340
|
+
}
|
|
341
|
+
buildExecArgs({ useInitialImages = false, schemaFilePath = "", lastMessageFilePath = "" } = {}) {
|
|
342
|
+
const args = [...this.baseArgs];
|
|
343
|
+
args.push("exec");
|
|
344
|
+
args.push("--json");
|
|
345
|
+
args.push("--color", "never");
|
|
346
|
+
args.push("--skip-git-repo-check");
|
|
347
|
+
args.push("--full-auto");
|
|
348
|
+
if (lastMessageFilePath) {
|
|
349
|
+
args.push("--output-last-message", lastMessageFilePath);
|
|
350
|
+
}
|
|
351
|
+
if (schemaFilePath) {
|
|
352
|
+
args.push("--output-schema", schemaFilePath);
|
|
353
|
+
}
|
|
354
|
+
if (typeof this.options.model === "string" && this.options.model.trim()) {
|
|
355
|
+
args.push("--model", this.options.model.trim());
|
|
356
|
+
}
|
|
357
|
+
const images = useInitialImages && Array.isArray(this.options.initialImages)
|
|
358
|
+
? this.options.initialImages.filter((item) => typeof item === "string" && item.trim())
|
|
359
|
+
: [];
|
|
360
|
+
for (const imagePath of images) {
|
|
361
|
+
args.push("--image", imagePath);
|
|
362
|
+
}
|
|
363
|
+
return args;
|
|
364
|
+
}
|
|
365
|
+
maybeEmitAuthRequired(stderrTail) {
|
|
366
|
+
const lastMessage = Array.isArray(stderrTail) ? String(stderrTail.filter(Boolean).at(-1) || "") : "";
|
|
367
|
+
const normalized = lastMessage.toLowerCase();
|
|
368
|
+
if (!normalized.includes("login") &&
|
|
369
|
+
!normalized.includes("auth") &&
|
|
370
|
+
!normalized.includes("api key") &&
|
|
371
|
+
!normalized.includes("credential")) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
this.emit("auth_required", {
|
|
375
|
+
reason: "login_required",
|
|
376
|
+
message: lastMessage || "Codex authentication required",
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
|
|
380
|
+
if (this.closeRequested || this.closed) {
|
|
381
|
+
throw this.createSessionClosedError();
|
|
382
|
+
}
|
|
383
|
+
if (this.currentTurn) {
|
|
384
|
+
throw createTurnError("Codex exec turn already running", {
|
|
385
|
+
reason: "turn_already_running",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
const effectivePrompt = this.buildPrompt(promptText);
|
|
389
|
+
const imagePaths = useInitialImages && Array.isArray(this.options.initialImages)
|
|
390
|
+
? this.options.initialImages.filter((item) => typeof item === "string" && item.trim())
|
|
391
|
+
: [];
|
|
392
|
+
const stdinPrompt = effectivePrompt || (imagePaths.length > 0 ? "Analyze the attached image." : "");
|
|
393
|
+
if (!stdinPrompt && imagePaths.length === 0) {
|
|
394
|
+
return buildEmptyTurnResult();
|
|
395
|
+
}
|
|
396
|
+
const normalizedSchema = normalizeJsonSchema(jsonSchema);
|
|
397
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "codex-exec-turn-"));
|
|
398
|
+
const lastMessageFilePath = path.join(tempDir, "last-message.txt");
|
|
399
|
+
const schemaFilePath = normalizedSchema ? path.join(tempDir, "schema.json") : "";
|
|
400
|
+
if (normalizedSchema) {
|
|
401
|
+
await fs.promises.writeFile(schemaFilePath, JSON.stringify(normalizedSchema, null, 2), "utf8");
|
|
402
|
+
}
|
|
403
|
+
this.announceSession();
|
|
404
|
+
this.history.push({ role: "user", content: String(promptText || "") });
|
|
405
|
+
const currentTurn = {
|
|
406
|
+
child: null,
|
|
407
|
+
stdoutEvents: [],
|
|
408
|
+
stderrTail: [],
|
|
409
|
+
settled: false,
|
|
410
|
+
};
|
|
411
|
+
this.currentTurn = currentTurn;
|
|
412
|
+
try {
|
|
413
|
+
await this.emitWorkingStatus({
|
|
414
|
+
phase: "turn_started",
|
|
415
|
+
reply_in_progress: true,
|
|
416
|
+
status_line: statusLineForPhase("turn_started"),
|
|
417
|
+
}, onProgress);
|
|
418
|
+
const args = this.buildExecArgs({
|
|
419
|
+
useInitialImages,
|
|
420
|
+
schemaFilePath,
|
|
421
|
+
lastMessageFilePath,
|
|
422
|
+
});
|
|
423
|
+
this.trace(`spawn ${[this.command, ...args].join(" ")} <stdin prompt>`);
|
|
424
|
+
const result = await new Promise((resolve, reject) => {
|
|
425
|
+
const child = spawn(this.command, args, {
|
|
426
|
+
cwd: this.cwd,
|
|
427
|
+
env: buildCodexExecEnv({
|
|
428
|
+
cwd: this.cwd,
|
|
429
|
+
extraEnv: this.env,
|
|
430
|
+
ignoreCodexApiKey: this.ignoreCodexApiKey,
|
|
431
|
+
}),
|
|
432
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
433
|
+
});
|
|
434
|
+
currentTurn.child = child;
|
|
435
|
+
const stdoutReader = readline.createInterface({ input: child.stdout });
|
|
436
|
+
const stderrReader = readline.createInterface({ input: child.stderr });
|
|
437
|
+
let timeoutId = null;
|
|
438
|
+
const settle = (error, value = null) => {
|
|
439
|
+
if (currentTurn.settled) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
currentTurn.settled = true;
|
|
443
|
+
if (timeoutId) {
|
|
444
|
+
clearTimeout(timeoutId);
|
|
445
|
+
}
|
|
446
|
+
stdoutReader.close();
|
|
447
|
+
stderrReader.close();
|
|
448
|
+
if (error) {
|
|
449
|
+
reject(error);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
resolve(value);
|
|
453
|
+
};
|
|
454
|
+
timeoutId = setTimeout(() => {
|
|
455
|
+
try {
|
|
456
|
+
child.kill("SIGTERM");
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// ignore
|
|
460
|
+
}
|
|
461
|
+
settle(createTurnError("Codex exec turn timed out", {
|
|
462
|
+
reason: "turn_timeout",
|
|
463
|
+
}));
|
|
464
|
+
}, this.turnDeadlineMs);
|
|
465
|
+
stdoutReader.on("line", (line) => {
|
|
466
|
+
const normalizedLine = String(line || "").trim();
|
|
467
|
+
if (!normalizedLine) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
let payload = null;
|
|
471
|
+
try {
|
|
472
|
+
payload = JSON.parse(normalizedLine);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
payload = { type: "raw", line: normalizedLine };
|
|
476
|
+
}
|
|
477
|
+
currentTurn.stdoutEvents.push(payload);
|
|
478
|
+
const phase = resolveExecPhase(payload);
|
|
479
|
+
if (phase) {
|
|
480
|
+
void this.emitWorkingStatus({
|
|
481
|
+
phase,
|
|
482
|
+
reply_in_progress: true,
|
|
483
|
+
status_line: statusLineForPhase(phase),
|
|
484
|
+
}, onProgress);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
stderrReader.on("line", (line) => {
|
|
488
|
+
const normalizedLine = String(line || "");
|
|
489
|
+
if (!normalizedLine.trim()) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
currentTurn.stderrTail.push(normalizedLine);
|
|
493
|
+
if (currentTurn.stderrTail.length > 20) {
|
|
494
|
+
currentTurn.stderrTail.shift();
|
|
495
|
+
}
|
|
496
|
+
this.writeLog(`[codex-exec] stderr ${sanitizeForLog(normalizedLine, 300)}`);
|
|
497
|
+
});
|
|
498
|
+
child.on("error", (error) => {
|
|
499
|
+
settle(error);
|
|
500
|
+
});
|
|
501
|
+
child.stdin.on("error", () => {
|
|
502
|
+
// best effort; the process may exit before reading stdin
|
|
503
|
+
});
|
|
504
|
+
child.on("exit", (code, signal) => {
|
|
505
|
+
if (this.closeRequested) {
|
|
506
|
+
settle(this.createSessionClosedError());
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (code !== 0) {
|
|
510
|
+
this.maybeEmitAuthRequired(currentTurn.stderrTail);
|
|
511
|
+
const stderrSummary = sanitizeForLog(currentTurn.stderrTail.filter(Boolean).at(-1), 200);
|
|
512
|
+
settle(createTurnError(stderrSummary ? `Codex exec failed: ${stderrSummary}` : "Codex exec failed", {
|
|
513
|
+
reason: "turn_failed",
|
|
514
|
+
code,
|
|
515
|
+
signal,
|
|
516
|
+
stderr: [...currentTurn.stderrTail],
|
|
517
|
+
}));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
settle(null, {
|
|
521
|
+
text: readTextFileIfExists(lastMessageFilePath),
|
|
522
|
+
events: [...currentTurn.stdoutEvents],
|
|
523
|
+
stderr: [...currentTurn.stderrTail],
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
child.stdin.end(stdinPrompt);
|
|
527
|
+
});
|
|
528
|
+
const text = typeof result?.text === "string" ? result.text : "";
|
|
529
|
+
if (text) {
|
|
530
|
+
this.history.push({ role: "assistant", content: text });
|
|
531
|
+
}
|
|
532
|
+
this.activeReplyTarget = "";
|
|
533
|
+
await this.emitWorkingStatus({
|
|
534
|
+
phase: "turn_completed",
|
|
535
|
+
reply_in_progress: false,
|
|
536
|
+
status_line: statusLineForPhase("turn_completed"),
|
|
537
|
+
}, onProgress);
|
|
538
|
+
await this.emitAssistantMessage(text);
|
|
539
|
+
return {
|
|
540
|
+
text,
|
|
541
|
+
usage: null,
|
|
542
|
+
items: result?.events || [],
|
|
543
|
+
events: result?.events || [],
|
|
544
|
+
provider: this.backend,
|
|
545
|
+
metadata: {
|
|
546
|
+
source: CODEX_EXEC_PROVIDER_VARIANT,
|
|
547
|
+
sessionId: this.sessionId || undefined,
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
if (!this.closeRequested && error?.reason !== "session_closed") {
|
|
553
|
+
await this.emitWorkingStatus({
|
|
554
|
+
phase: "turn_failed",
|
|
555
|
+
reply_in_progress: false,
|
|
556
|
+
status_line: statusLineForPhase("turn_failed"),
|
|
557
|
+
status_done_line: error?.message || "Codex exec failed",
|
|
558
|
+
}, onProgress);
|
|
559
|
+
}
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
this.currentTurn = null;
|
|
564
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async close() {
|
|
568
|
+
if (this.closed) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.closeRequested = true;
|
|
572
|
+
this.closed = true;
|
|
573
|
+
const child = this.currentTurn?.child;
|
|
574
|
+
if (child) {
|
|
575
|
+
try {
|
|
576
|
+
child.kill("SIGTERM");
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// ignore
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|