cyrus-codex-runner 0.2.64-test.6 → 0.2.64
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/CodexEventMapper.d.ts +56 -0
- package/dist/CodexEventMapper.d.ts.map +1 -0
- package/dist/CodexEventMapper.js +469 -0
- package/dist/CodexEventMapper.js.map +1 -0
- package/dist/CodexRunner.d.ts +37 -46
- package/dist/CodexRunner.d.ts.map +1 -1
- package/dist/CodexRunner.js +136 -851
- package/dist/CodexRunner.js.map +1 -1
- package/dist/CodexSkillStager.d.ts +42 -0
- package/dist/CodexSkillStager.d.ts.map +1 -0
- package/dist/CodexSkillStager.js +182 -0
- package/dist/CodexSkillStager.js.map +1 -0
- package/dist/backend/AppServerCodexBackend.d.ts +74 -0
- package/dist/backend/AppServerCodexBackend.d.ts.map +1 -0
- package/dist/backend/AppServerCodexBackend.js +352 -0
- package/dist/backend/AppServerCodexBackend.js.map +1 -0
- package/dist/backend/appServerClient.d.ts +75 -0
- package/dist/backend/appServerClient.d.ts.map +1 -0
- package/dist/backend/appServerClient.js +223 -0
- package/dist/backend/appServerClient.js.map +1 -0
- package/dist/backend/appServerEvents.d.ts +9 -0
- package/dist/backend/appServerEvents.d.ts.map +1 -0
- package/dist/backend/appServerEvents.js +110 -0
- package/dist/backend/appServerEvents.js.map +1 -0
- package/dist/backend/appServerProcess.d.ts +38 -0
- package/dist/backend/appServerProcess.d.ts.map +1 -0
- package/dist/backend/appServerProcess.js +283 -0
- package/dist/backend/appServerProcess.js.map +1 -0
- package/dist/backend/codexBinary.d.ts +24 -0
- package/dist/backend/codexBinary.d.ts.map +1 -0
- package/dist/backend/codexBinary.js +44 -0
- package/dist/backend/codexBinary.js.map +1 -0
- package/dist/backend/types.d.ts +210 -0
- package/dist/backend/types.d.ts.map +1 -0
- package/dist/backend/types.js +2 -0
- package/dist/backend/types.js.map +1 -0
- package/dist/config/CodexConfigBuilder.d.ts +40 -0
- package/dist/config/CodexConfigBuilder.d.ts.map +1 -0
- package/dist/config/CodexConfigBuilder.js +182 -0
- package/dist/config/CodexConfigBuilder.js.map +1 -0
- package/dist/config/mcpConfigTranslator.d.ts +17 -0
- package/dist/config/mcpConfigTranslator.d.ts.map +1 -0
- package/dist/config/mcpConfigTranslator.js +245 -0
- package/dist/config/mcpConfigTranslator.js.map +1 -0
- package/dist/config/sandboxPolicy.d.ts +43 -0
- package/dist/config/sandboxPolicy.d.ts.map +1 -0
- package/dist/config/sandboxPolicy.js +56 -0
- package/dist/config/sandboxPolicy.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +9 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -4
package/dist/CodexRunner.js
CHANGED
|
@@ -1,297 +1,48 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { Codex } from "@openai/codex-sdk";
|
|
3
|
+
import { AppServerCodexBackend } from "./backend/AppServerCodexBackend.js";
|
|
4
|
+
import { CodexEventMapper } from "./CodexEventMapper.js";
|
|
5
|
+
import { CodexSkillStager } from "./CodexSkillStager.js";
|
|
6
|
+
import { CodexConfigBuilder } from "./config/CodexConfigBuilder.js";
|
|
8
7
|
import { CodexMessageFormatter } from "./formatter.js";
|
|
9
|
-
const DEFAULT_CODEX_MODEL = "gpt-5.5";
|
|
10
|
-
const CODEX_MCP_DOCS_URL = "https://platform.openai.com/docs/docs-mcp";
|
|
11
|
-
function toFiniteNumber(value) {
|
|
12
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
13
|
-
}
|
|
14
|
-
function safeStringify(value) {
|
|
15
|
-
try {
|
|
16
|
-
return JSON.stringify(value, null, 2);
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return String(value);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
function createAssistantToolUseMessage(toolUseId, toolName, toolInput, messageId = crypto.randomUUID()) {
|
|
23
|
-
const contentBlocks = [
|
|
24
|
-
{
|
|
25
|
-
type: "tool_use",
|
|
26
|
-
id: toolUseId,
|
|
27
|
-
name: toolName,
|
|
28
|
-
input: toolInput,
|
|
29
|
-
},
|
|
30
|
-
];
|
|
31
|
-
return {
|
|
32
|
-
id: messageId,
|
|
33
|
-
type: "message",
|
|
34
|
-
role: "assistant",
|
|
35
|
-
content: contentBlocks,
|
|
36
|
-
model: DEFAULT_CODEX_MODEL,
|
|
37
|
-
stop_reason: null,
|
|
38
|
-
stop_sequence: null,
|
|
39
|
-
stop_details: null,
|
|
40
|
-
usage: {
|
|
41
|
-
input_tokens: 0,
|
|
42
|
-
output_tokens: 0,
|
|
43
|
-
cache_creation_input_tokens: 0,
|
|
44
|
-
cache_read_input_tokens: 0,
|
|
45
|
-
output_tokens_details: null,
|
|
46
|
-
cache_creation: null,
|
|
47
|
-
inference_geo: null,
|
|
48
|
-
iterations: null,
|
|
49
|
-
server_tool_use: null,
|
|
50
|
-
service_tier: null,
|
|
51
|
-
speed: null,
|
|
52
|
-
},
|
|
53
|
-
container: null,
|
|
54
|
-
context_management: null,
|
|
55
|
-
diagnostics: null,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
function createUserToolResultMessage(toolUseId, result, isError) {
|
|
59
|
-
const contentBlocks = [
|
|
60
|
-
{
|
|
61
|
-
type: "tool_result",
|
|
62
|
-
tool_use_id: toolUseId,
|
|
63
|
-
content: result,
|
|
64
|
-
is_error: isError,
|
|
65
|
-
},
|
|
66
|
-
];
|
|
67
|
-
return {
|
|
68
|
-
role: "user",
|
|
69
|
-
content: contentBlocks,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
function createAssistantBetaMessage(content, messageId = crypto.randomUUID()) {
|
|
73
|
-
const contentBlocks = [
|
|
74
|
-
{ type: "text", text: content },
|
|
75
|
-
];
|
|
76
|
-
return {
|
|
77
|
-
id: messageId,
|
|
78
|
-
type: "message",
|
|
79
|
-
role: "assistant",
|
|
80
|
-
content: contentBlocks,
|
|
81
|
-
model: DEFAULT_CODEX_MODEL,
|
|
82
|
-
stop_reason: null,
|
|
83
|
-
stop_sequence: null,
|
|
84
|
-
stop_details: null,
|
|
85
|
-
usage: {
|
|
86
|
-
input_tokens: 0,
|
|
87
|
-
output_tokens: 0,
|
|
88
|
-
cache_creation_input_tokens: 0,
|
|
89
|
-
cache_read_input_tokens: 0,
|
|
90
|
-
output_tokens_details: null,
|
|
91
|
-
cache_creation: null,
|
|
92
|
-
inference_geo: null,
|
|
93
|
-
iterations: null,
|
|
94
|
-
server_tool_use: null,
|
|
95
|
-
service_tier: null,
|
|
96
|
-
speed: null,
|
|
97
|
-
},
|
|
98
|
-
container: null,
|
|
99
|
-
context_management: null,
|
|
100
|
-
diagnostics: null,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
function parseUsage(usage) {
|
|
104
|
-
if (!usage) {
|
|
105
|
-
return {
|
|
106
|
-
inputTokens: 0,
|
|
107
|
-
outputTokens: 0,
|
|
108
|
-
cachedInputTokens: 0,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
inputTokens: toFiniteNumber(usage.input_tokens),
|
|
113
|
-
outputTokens: toFiniteNumber(usage.output_tokens),
|
|
114
|
-
cachedInputTokens: toFiniteNumber(usage.cached_input_tokens),
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
function createResultUsage(parsed) {
|
|
118
|
-
return {
|
|
119
|
-
input_tokens: parsed.inputTokens,
|
|
120
|
-
output_tokens: parsed.outputTokens,
|
|
121
|
-
cache_creation_input_tokens: 0,
|
|
122
|
-
cache_read_input_tokens: parsed.cachedInputTokens,
|
|
123
|
-
output_tokens_details: { thinking_tokens: 0 },
|
|
124
|
-
cache_creation: {
|
|
125
|
-
ephemeral_1h_input_tokens: 0,
|
|
126
|
-
ephemeral_5m_input_tokens: 0,
|
|
127
|
-
},
|
|
128
|
-
inference_geo: "unknown",
|
|
129
|
-
iterations: [],
|
|
130
|
-
server_tool_use: {
|
|
131
|
-
web_fetch_requests: 0,
|
|
132
|
-
web_search_requests: 0,
|
|
133
|
-
},
|
|
134
|
-
service_tier: "standard",
|
|
135
|
-
speed: "standard",
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
function getDefaultReasoningEffortForModel(model) {
|
|
139
|
-
// All gpt-5 variants (including plain "gpt-5") reject xhigh; pin to "high".
|
|
140
|
-
return /^gpt-5/i.test(model || "") ? "high" : undefined;
|
|
141
|
-
}
|
|
142
|
-
function normalizeError(error) {
|
|
143
|
-
if (error instanceof Error) {
|
|
144
|
-
return error.message;
|
|
145
|
-
}
|
|
146
|
-
if (typeof error === "string") {
|
|
147
|
-
return error;
|
|
148
|
-
}
|
|
149
|
-
return "Codex execution failed";
|
|
150
|
-
}
|
|
151
|
-
function inferCommandToolName(command) {
|
|
152
|
-
const normalized = command.toLowerCase();
|
|
153
|
-
if (/\brg\b|\bgrep\b/.test(normalized)) {
|
|
154
|
-
return "Grep";
|
|
155
|
-
}
|
|
156
|
-
if (/\bglob\.glob\b|\bfind\b.+\s-name\s/.test(normalized)) {
|
|
157
|
-
return "Glob";
|
|
158
|
-
}
|
|
159
|
-
if (/\bcat\b/.test(normalized) && !/>/.test(normalized)) {
|
|
160
|
-
return "Read";
|
|
161
|
-
}
|
|
162
|
-
if (/<<\s*['"]?eof['"]?\s*>/i.test(command) ||
|
|
163
|
-
/\becho\b.+>/.test(normalized)) {
|
|
164
|
-
return "Write";
|
|
165
|
-
}
|
|
166
|
-
return "Bash";
|
|
167
|
-
}
|
|
168
|
-
function normalizeFilePath(path, workingDirectory) {
|
|
169
|
-
if (!path) {
|
|
170
|
-
return path;
|
|
171
|
-
}
|
|
172
|
-
if (workingDirectory && path.startsWith(workingDirectory)) {
|
|
173
|
-
const relativePath = pathRelative(workingDirectory, path);
|
|
174
|
-
if (relativePath && relativePath !== ".") {
|
|
175
|
-
return relativePath;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return path;
|
|
179
|
-
}
|
|
180
|
-
function summarizeFileChanges(item, workingDirectory) {
|
|
181
|
-
if (!item.changes.length) {
|
|
182
|
-
return item.status === "failed" ? "Patch failed" : "No file changes";
|
|
183
|
-
}
|
|
184
|
-
return item.changes
|
|
185
|
-
.map((change) => {
|
|
186
|
-
const filePath = normalizeFilePath(change.path, workingDirectory);
|
|
187
|
-
return `${change.kind} ${filePath}`;
|
|
188
|
-
})
|
|
189
|
-
.join("\n");
|
|
190
|
-
}
|
|
191
|
-
function asRecord(value) {
|
|
192
|
-
if (value && typeof value === "object") {
|
|
193
|
-
return value;
|
|
194
|
-
}
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
function toMcpResultString(item) {
|
|
198
|
-
if (item.error?.message) {
|
|
199
|
-
return item.error.message;
|
|
200
|
-
}
|
|
201
|
-
const textBlocks = [];
|
|
202
|
-
for (const block of item.result?.content || []) {
|
|
203
|
-
const text = asRecord(block)?.text;
|
|
204
|
-
if (typeof text === "string" && text.trim().length > 0) {
|
|
205
|
-
textBlocks.push(text);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if (textBlocks.length > 0) {
|
|
209
|
-
return textBlocks.join("\n");
|
|
210
|
-
}
|
|
211
|
-
if (item.result?.structured_content !== undefined) {
|
|
212
|
-
return safeStringify(item.result.structured_content);
|
|
213
|
-
}
|
|
214
|
-
return item.status === "failed"
|
|
215
|
-
? "MCP tool call failed"
|
|
216
|
-
: "MCP tool call completed";
|
|
217
|
-
}
|
|
218
|
-
function normalizeMcpIdentifier(value) {
|
|
219
|
-
const normalized = value
|
|
220
|
-
.toLowerCase()
|
|
221
|
-
.replace(/[^a-z0-9_]+/g, "_")
|
|
222
|
-
.replace(/^_+|_+$/g, "");
|
|
223
|
-
return normalized || "unknown";
|
|
224
|
-
}
|
|
225
|
-
function autoDetectMcpConfigPath(workingDirectory) {
|
|
226
|
-
if (!workingDirectory) {
|
|
227
|
-
return undefined;
|
|
228
|
-
}
|
|
229
|
-
const mcpPath = join(workingDirectory, ".mcp.json");
|
|
230
|
-
if (!existsSync(mcpPath)) {
|
|
231
|
-
return undefined;
|
|
232
|
-
}
|
|
233
|
-
try {
|
|
234
|
-
JSON.parse(readFileSync(mcpPath, "utf8"));
|
|
235
|
-
return mcpPath;
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
console.warn(`[CodexRunner] Found .mcp.json at ${mcpPath} but it is invalid JSON, skipping`);
|
|
239
|
-
return undefined;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
function loadMcpConfigFromPaths(configPaths) {
|
|
243
|
-
if (!configPaths) {
|
|
244
|
-
return {};
|
|
245
|
-
}
|
|
246
|
-
const paths = Array.isArray(configPaths) ? configPaths : [configPaths];
|
|
247
|
-
let mcpServers = {};
|
|
248
|
-
for (const configPath of paths) {
|
|
249
|
-
try {
|
|
250
|
-
const mcpConfigContent = readFileSync(configPath, "utf8");
|
|
251
|
-
const mcpConfig = JSON.parse(mcpConfigContent);
|
|
252
|
-
const servers = mcpConfig &&
|
|
253
|
-
typeof mcpConfig === "object" &&
|
|
254
|
-
!Array.isArray(mcpConfig) &&
|
|
255
|
-
mcpConfig.mcpServers &&
|
|
256
|
-
typeof mcpConfig.mcpServers === "object" &&
|
|
257
|
-
!Array.isArray(mcpConfig.mcpServers)
|
|
258
|
-
? mcpConfig.mcpServers
|
|
259
|
-
: {};
|
|
260
|
-
mcpServers = { ...mcpServers, ...servers };
|
|
261
|
-
console.log(`[CodexRunner] Loaded MCP config from ${configPath}: ${Object.keys(servers).join(", ")}`);
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
console.warn(`[CodexRunner] Failed to load MCP config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return mcpServers;
|
|
268
|
-
}
|
|
269
8
|
/**
|
|
270
|
-
*
|
|
9
|
+
* Adapts Codex to Cyrus's {@link IAgentRunner} contract.
|
|
10
|
+
*
|
|
11
|
+
* The runner is a thin orchestrator: it owns session lifecycle and delegates
|
|
12
|
+
* configuration assembly ({@link CodexConfigBuilder}), skill staging
|
|
13
|
+
* ({@link CodexSkillStager}), event→message mapping ({@link CodexEventMapper}),
|
|
14
|
+
* and transport ({@link CodexBackend}) to dedicated collaborators. Codex is
|
|
15
|
+
* driven exclusively through the app-server backend, which supports mid-turn
|
|
16
|
+
* input injection (`turn/steer`).
|
|
271
17
|
*/
|
|
272
18
|
export class CodexRunner extends EventEmitter {
|
|
273
|
-
supportsStreamingInput =
|
|
19
|
+
supportsStreamingInput = true;
|
|
274
20
|
config;
|
|
275
|
-
sessionInfo = null;
|
|
276
|
-
messages = [];
|
|
277
21
|
formatter;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
inputTokens: 0,
|
|
283
|
-
outputTokens: 0,
|
|
284
|
-
cachedInputTokens: 0,
|
|
285
|
-
};
|
|
286
|
-
errorMessages = [];
|
|
287
|
-
startTimestampMs = 0;
|
|
22
|
+
skillStager;
|
|
23
|
+
mapper;
|
|
24
|
+
sessionInfo = null;
|
|
25
|
+
backend = null;
|
|
288
26
|
wasStopped = false;
|
|
289
|
-
|
|
290
|
-
|
|
27
|
+
/** Set once the turn reaches a terminal state; gates {@link isStreaming}. */
|
|
28
|
+
turnFinished = false;
|
|
29
|
+
/**
|
|
30
|
+
* Follow-up messages that arrived before the turn became steerable (during
|
|
31
|
+
* config build / process spawn / thread start). Flushed via `steer` once the
|
|
32
|
+
* turn starts, so a fast follow-up is never lost or wrongly deferred.
|
|
33
|
+
*/
|
|
34
|
+
pendingFollowups = [];
|
|
291
35
|
constructor(config) {
|
|
292
36
|
super();
|
|
293
37
|
this.config = config;
|
|
294
38
|
this.formatter = new CodexMessageFormatter();
|
|
39
|
+
this.skillStager = new CodexSkillStager({
|
|
40
|
+
workingDirectory: config.workingDirectory,
|
|
41
|
+
additionalDirectories: config.additionalDirectories,
|
|
42
|
+
skills: config.skills,
|
|
43
|
+
plugins: config.plugins,
|
|
44
|
+
});
|
|
45
|
+
this.mapper = new CodexEventMapper(this.buildMapperContext());
|
|
295
46
|
if (config.onMessage)
|
|
296
47
|
this.on("message", config.onMessage);
|
|
297
48
|
if (config.onError)
|
|
@@ -303,49 +54,79 @@ export class CodexRunner extends EventEmitter {
|
|
|
303
54
|
return this.startWithPrompt(prompt);
|
|
304
55
|
}
|
|
305
56
|
async startStreaming(initialPrompt) {
|
|
306
|
-
return this.startWithPrompt(
|
|
57
|
+
return this.startWithPrompt(initialPrompt);
|
|
307
58
|
}
|
|
308
|
-
|
|
309
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Inject a message mid-session. While a turn is steerable it is sent
|
|
61
|
+
* immediately (`turn/steer`); during the startup window (before the turn
|
|
62
|
+
* begins) it is buffered and flushed once the turn starts. Throws only once
|
|
63
|
+
* the turn has finished, where the caller should resume with a new turn.
|
|
64
|
+
*/
|
|
65
|
+
addStreamMessage(content) {
|
|
66
|
+
if (this.backend?.isTurnActive()) {
|
|
67
|
+
this.steer(content);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (this.isRunning() && !this.turnFinished) {
|
|
71
|
+
this.pendingFollowups.push(content);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
throw new Error("Cannot stream message: no active Codex turn");
|
|
310
75
|
}
|
|
311
76
|
completeStream() {
|
|
312
|
-
// No-op:
|
|
77
|
+
// No-op: each turn's input is delivered up front (or via steer); there is
|
|
78
|
+
// no open input stream to close.
|
|
79
|
+
}
|
|
80
|
+
isStreaming() {
|
|
81
|
+
// True for the whole running, not-yet-finished window — including the
|
|
82
|
+
// startup gap before the turn is active — so callers stream follow-ups in
|
|
83
|
+
// (buffered if needed) rather than deferring them.
|
|
84
|
+
return (this.supportsStreamingInput && this.isRunning() && !this.turnFinished);
|
|
85
|
+
}
|
|
86
|
+
stop() {
|
|
87
|
+
if (this.sessionInfo?.isRunning) {
|
|
88
|
+
this.wasStopped = true;
|
|
89
|
+
}
|
|
90
|
+
this.cleanupRuntimeState();
|
|
91
|
+
}
|
|
92
|
+
isRunning() {
|
|
93
|
+
return this.sessionInfo?.isRunning ?? false;
|
|
313
94
|
}
|
|
314
|
-
|
|
95
|
+
getMessages() {
|
|
96
|
+
return this.mapper.getMessages();
|
|
97
|
+
}
|
|
98
|
+
getFormatter() {
|
|
99
|
+
return this.formatter;
|
|
100
|
+
}
|
|
101
|
+
// ---- internals ----------------------------------------------------------
|
|
102
|
+
async startWithPrompt(prompt) {
|
|
315
103
|
if (this.isRunning()) {
|
|
316
104
|
throw new Error("Codex session already running");
|
|
317
105
|
}
|
|
318
|
-
const sessionId = this.config.resumeSessionId || crypto.randomUUID();
|
|
319
106
|
this.sessionInfo = {
|
|
320
|
-
sessionId,
|
|
107
|
+
sessionId: this.config.resumeSessionId || crypto.randomUUID(),
|
|
321
108
|
startedAt: new Date(),
|
|
322
109
|
isRunning: true,
|
|
323
110
|
};
|
|
324
|
-
this.messages = [];
|
|
325
|
-
this.hasInitMessage = false;
|
|
326
|
-
this.pendingResultMessage = null;
|
|
327
|
-
this.lastAssistantText = null;
|
|
328
|
-
this.lastUsage = {
|
|
329
|
-
inputTokens: 0,
|
|
330
|
-
outputTokens: 0,
|
|
331
|
-
cachedInputTokens: 0,
|
|
332
|
-
};
|
|
333
|
-
this.errorMessages = [];
|
|
334
111
|
this.wasStopped = false;
|
|
335
|
-
this.
|
|
336
|
-
this.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
this.
|
|
112
|
+
this.turnFinished = false;
|
|
113
|
+
this.pendingFollowups = [];
|
|
114
|
+
this.mapper.reset();
|
|
115
|
+
// Create the backend up front (before the slow config build / process
|
|
116
|
+
// spawn) so addStreamMessage can buffer follow-ups that arrive during the
|
|
117
|
+
// startup window rather than throwing.
|
|
118
|
+
const backend = this.createBackend();
|
|
119
|
+
this.backend = backend;
|
|
120
|
+
backend.on("event", (event) => this.handleBackendEvent(event));
|
|
121
|
+
const resolved = await new CodexConfigBuilder(this.config).build();
|
|
122
|
+
this.skillStager.stage();
|
|
123
|
+
const input = prompt?.trim()
|
|
124
|
+
? [{ type: "text", text: prompt.trim() }]
|
|
125
|
+
: [];
|
|
346
126
|
let caughtError;
|
|
347
127
|
try {
|
|
348
|
-
await
|
|
128
|
+
await backend.open(resolved);
|
|
129
|
+
await backend.runTurn(input);
|
|
349
130
|
}
|
|
350
131
|
catch (error) {
|
|
351
132
|
caughtError = error;
|
|
@@ -355,440 +136,52 @@ export class CodexRunner extends EventEmitter {
|
|
|
355
136
|
}
|
|
356
137
|
return this.sessionInfo;
|
|
357
138
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
* If not, swap to the fallback model before starting the session.
|
|
361
|
-
*
|
|
362
|
-
* Skipped when:
|
|
363
|
-
* - No OPENAI_API_KEY is set (Codex-native auth handles model access)
|
|
364
|
-
* - The user has a ChatGPT subscription (`codex login status` reports "Logged in using ChatGPT")
|
|
365
|
-
*/
|
|
366
|
-
async resolveModelWithFallback() {
|
|
367
|
-
const model = this.config.model;
|
|
368
|
-
const fallback = this.config.fallbackModel;
|
|
369
|
-
if (!model || !fallback || fallback === model)
|
|
370
|
-
return;
|
|
371
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
372
|
-
if (!apiKey)
|
|
373
|
-
return;
|
|
374
|
-
if (await this.hasCodexSubscription())
|
|
375
|
-
return;
|
|
376
|
-
const baseUrl = (process.env.OPENAI_BASE_URL ||
|
|
377
|
-
process.env.OPENAI_API_BASE ||
|
|
378
|
-
"https://api.openai.com/v1").replace(/\/+$/, "");
|
|
379
|
-
try {
|
|
380
|
-
const response = await fetch(`${baseUrl}/models/${encodeURIComponent(model)}`, {
|
|
381
|
-
method: "GET",
|
|
382
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
383
|
-
signal: AbortSignal.timeout(10_000),
|
|
384
|
-
});
|
|
385
|
-
if (response.status === 404) {
|
|
386
|
-
console.log(`[CodexRunner] Model "${model}" not found (404), falling back to "${fallback}"`);
|
|
387
|
-
this.config.model = fallback;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
catch {
|
|
391
|
-
// Network error or timeout — proceed with the original model
|
|
392
|
-
// and let the Codex SDK handle any downstream failure.
|
|
393
|
-
}
|
|
139
|
+
createBackend() {
|
|
140
|
+
return new AppServerCodexBackend();
|
|
394
141
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
*/
|
|
400
|
-
async hasCodexSubscription() {
|
|
401
|
-
const codexBin = this.config.codexPath || "codex";
|
|
402
|
-
try {
|
|
403
|
-
const { execFile } = await import("node:child_process");
|
|
404
|
-
const { promisify } = await import("node:util");
|
|
405
|
-
const execFileAsync = promisify(execFile);
|
|
406
|
-
const { stdout, stderr } = await execFileAsync(codexBin, ["login", "status"], { timeout: 5_000 });
|
|
407
|
-
const result = /logged in using chatgpt/i.test(stdout + stderr);
|
|
408
|
-
console.log(`[CodexRunner] hasCodexSubscription: ${result} (stdout: "${stdout.trim()}"${stderr.trim() ? `, stderr: "${stderr.trim()}"` : ""})`);
|
|
409
|
-
return result;
|
|
142
|
+
handleBackendEvent(event) {
|
|
143
|
+
if (event.kind === "turn-started") {
|
|
144
|
+
// Turn is now steerable — deliver anything buffered during startup.
|
|
145
|
+
this.flushPendingFollowups();
|
|
410
146
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
147
|
+
else if (event.kind === "turn-completed" ||
|
|
148
|
+
event.kind === "turn-failed") {
|
|
149
|
+
this.turnFinished = true;
|
|
414
150
|
}
|
|
151
|
+
this.mapper.handle(event);
|
|
415
152
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
...(this.config.codexPath
|
|
422
|
-
? { codexPathOverride: this.config.codexPath }
|
|
423
|
-
: {}),
|
|
424
|
-
...(envOverride ? { env: envOverride } : {}),
|
|
425
|
-
...(configOverrides ? { config: configOverrides } : {}),
|
|
153
|
+
steer(content) {
|
|
154
|
+
void this.backend
|
|
155
|
+
?.steer?.([{ type: "text", text: content }])
|
|
156
|
+
.catch((error) => {
|
|
157
|
+
this.emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
426
158
|
});
|
|
427
159
|
}
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
(this.config.includeWebSearch ? "live" : undefined);
|
|
434
|
-
const threadOptions = {
|
|
435
|
-
model: this.config.model,
|
|
436
|
-
sandboxMode: this.config.sandbox || "workspace-write",
|
|
437
|
-
workingDirectory: this.config.workingDirectory,
|
|
438
|
-
skipGitRepoCheck: this.config.skipGitRepoCheck ?? true,
|
|
439
|
-
approvalPolicy: this.config.askForApproval || "never",
|
|
440
|
-
...(reasoningEffort ? { modelReasoningEffort: reasoningEffort } : {}),
|
|
441
|
-
...(webSearchMode ? { webSearchMode } : {}),
|
|
442
|
-
...(additionalDirectories.length > 0 ? { additionalDirectories } : {}),
|
|
443
|
-
};
|
|
444
|
-
return threadOptions;
|
|
445
|
-
}
|
|
446
|
-
getAdditionalDirectories() {
|
|
447
|
-
const workingDirectory = this.config.workingDirectory;
|
|
448
|
-
const uniqueDirectories = new Set();
|
|
449
|
-
for (const directory of this.config.allowedDirectories || []) {
|
|
450
|
-
if (!directory || directory === workingDirectory) {
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
uniqueDirectories.add(directory);
|
|
160
|
+
flushPendingFollowups() {
|
|
161
|
+
const queued = this.pendingFollowups;
|
|
162
|
+
this.pendingFollowups = [];
|
|
163
|
+
for (const content of queued) {
|
|
164
|
+
this.steer(content);
|
|
454
165
|
}
|
|
455
|
-
return [...uniqueDirectories];
|
|
456
|
-
}
|
|
457
|
-
resolveCodexHome() {
|
|
458
|
-
const codexHome = this.config.codexHome ||
|
|
459
|
-
process.env.CODEX_HOME ||
|
|
460
|
-
join(homedir(), ".codex");
|
|
461
|
-
mkdirSync(codexHome, { recursive: true });
|
|
462
|
-
return codexHome;
|
|
463
166
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
return undefined;
|
|
467
|
-
}
|
|
468
|
-
const env = {};
|
|
469
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
470
|
-
if (typeof value === "string") {
|
|
471
|
-
env[key] = value;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
env.CODEX_HOME = codexHome;
|
|
475
|
-
return env;
|
|
476
|
-
}
|
|
477
|
-
buildCodexMcpServersConfig() {
|
|
478
|
-
const autoDetectedPath = autoDetectMcpConfigPath(this.config.workingDirectory);
|
|
479
|
-
const configPaths = autoDetectedPath
|
|
480
|
-
? [autoDetectedPath]
|
|
481
|
-
: [];
|
|
482
|
-
if (this.config.mcpConfigPath) {
|
|
483
|
-
const explicitPaths = Array.isArray(this.config.mcpConfigPath)
|
|
484
|
-
? this.config.mcpConfigPath
|
|
485
|
-
: [this.config.mcpConfigPath];
|
|
486
|
-
configPaths.push(...explicitPaths);
|
|
487
|
-
}
|
|
488
|
-
const fileBasedServers = loadMcpConfigFromPaths(configPaths);
|
|
489
|
-
const mergedServers = this.config.mcpConfig
|
|
490
|
-
? { ...fileBasedServers, ...this.config.mcpConfig }
|
|
491
|
-
: fileBasedServers;
|
|
492
|
-
if (Object.keys(mergedServers).length === 0) {
|
|
493
|
-
return undefined;
|
|
494
|
-
}
|
|
495
|
-
// Codex MCP configuration reference:
|
|
496
|
-
// https://platform.openai.com/docs/docs-mcp
|
|
497
|
-
const codexServers = {};
|
|
498
|
-
for (const [serverName, rawConfig] of Object.entries(mergedServers)) {
|
|
499
|
-
const configAny = rawConfig;
|
|
500
|
-
if (typeof configAny.listTools === "function" ||
|
|
501
|
-
typeof configAny.callTool === "function") {
|
|
502
|
-
console.warn(`[CodexRunner] Skipping MCP server '${serverName}' because in-process SDK server instances cannot be mapped to codex config`);
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
const mapped = {};
|
|
506
|
-
if (typeof configAny.command === "string") {
|
|
507
|
-
mapped.command = configAny.command;
|
|
508
|
-
}
|
|
509
|
-
if (Array.isArray(configAny.args)) {
|
|
510
|
-
mapped.args =
|
|
511
|
-
configAny.args;
|
|
512
|
-
}
|
|
513
|
-
if (configAny.env &&
|
|
514
|
-
typeof configAny.env === "object" &&
|
|
515
|
-
!Array.isArray(configAny.env)) {
|
|
516
|
-
mapped.env =
|
|
517
|
-
configAny.env;
|
|
518
|
-
}
|
|
519
|
-
if (typeof configAny.cwd === "string") {
|
|
520
|
-
mapped.cwd = configAny.cwd;
|
|
521
|
-
}
|
|
522
|
-
if (typeof configAny.url === "string") {
|
|
523
|
-
mapped.url = configAny.url;
|
|
524
|
-
}
|
|
525
|
-
if (configAny.http_headers &&
|
|
526
|
-
typeof configAny.http_headers === "object" &&
|
|
527
|
-
!Array.isArray(configAny.http_headers)) {
|
|
528
|
-
mapped.http_headers =
|
|
529
|
-
configAny.http_headers;
|
|
530
|
-
}
|
|
531
|
-
if (configAny.headers &&
|
|
532
|
-
typeof configAny.headers === "object" &&
|
|
533
|
-
!Array.isArray(configAny.headers)) {
|
|
534
|
-
mapped.http_headers =
|
|
535
|
-
configAny.headers;
|
|
536
|
-
}
|
|
537
|
-
if (configAny.env_http_headers &&
|
|
538
|
-
typeof configAny.env_http_headers === "object" &&
|
|
539
|
-
!Array.isArray(configAny.env_http_headers)) {
|
|
540
|
-
mapped.env_http_headers =
|
|
541
|
-
configAny.env_http_headers;
|
|
542
|
-
}
|
|
543
|
-
if (typeof configAny.bearer_token_env_var === "string") {
|
|
544
|
-
mapped.bearer_token_env_var = configAny.bearer_token_env_var;
|
|
545
|
-
}
|
|
546
|
-
if (typeof configAny.timeout === "number") {
|
|
547
|
-
mapped.timeout = configAny.timeout;
|
|
548
|
-
}
|
|
549
|
-
if (!mapped.command && !mapped.url) {
|
|
550
|
-
console.warn(`[CodexRunner] Skipping MCP server '${serverName}' because it has no command/url transport`);
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
codexServers[serverName] = mapped;
|
|
554
|
-
}
|
|
555
|
-
if (Object.keys(codexServers).length === 0) {
|
|
556
|
-
return undefined;
|
|
557
|
-
}
|
|
558
|
-
console.log(`[CodexRunner] Configured ${Object.keys(codexServers).length} MCP server(s) for codex config (docs: ${CODEX_MCP_DOCS_URL})`);
|
|
559
|
-
return codexServers;
|
|
560
|
-
}
|
|
561
|
-
buildConfigOverrides() {
|
|
562
|
-
const appendSystemPrompt = (this.config.appendSystemPrompt ?? "").trim();
|
|
563
|
-
const configOverrides = this.config.configOverrides
|
|
564
|
-
? { ...this.config.configOverrides }
|
|
565
|
-
: {};
|
|
566
|
-
const mcpServers = this.buildCodexMcpServersConfig();
|
|
567
|
-
if (mcpServers) {
|
|
568
|
-
const existingMcpServers = configOverrides.mcp_servers;
|
|
569
|
-
if (existingMcpServers &&
|
|
570
|
-
typeof existingMcpServers === "object" &&
|
|
571
|
-
!Array.isArray(existingMcpServers)) {
|
|
572
|
-
configOverrides.mcp_servers = {
|
|
573
|
-
...existingMcpServers,
|
|
574
|
-
...mcpServers,
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
else {
|
|
578
|
-
configOverrides.mcp_servers = mcpServers;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
const sandboxWorkspaceWrite = configOverrides.sandbox_workspace_write;
|
|
582
|
-
// Keep workspace-write as the default sandbox, but enable outbound network so
|
|
583
|
-
// common remote workflows (for example `git`/`gh` against GitHub) work without
|
|
584
|
-
// requiring danger-full-access.
|
|
585
|
-
if (sandboxWorkspaceWrite &&
|
|
586
|
-
typeof sandboxWorkspaceWrite === "object" &&
|
|
587
|
-
!Array.isArray(sandboxWorkspaceWrite)) {
|
|
588
|
-
configOverrides.sandbox_workspace_write = {
|
|
589
|
-
...sandboxWorkspaceWrite,
|
|
590
|
-
network_access: sandboxWorkspaceWrite
|
|
591
|
-
.network_access ?? true,
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
else if (!sandboxWorkspaceWrite) {
|
|
595
|
-
configOverrides.sandbox_workspace_write = { network_access: true };
|
|
596
|
-
}
|
|
597
|
-
if (!appendSystemPrompt) {
|
|
598
|
-
return Object.keys(configOverrides).length > 0
|
|
599
|
-
? configOverrides
|
|
600
|
-
: undefined;
|
|
601
|
-
}
|
|
167
|
+
buildMapperContext() {
|
|
168
|
+
const self = this;
|
|
602
169
|
return {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
this.handleEvent(event);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
handleEvent(event) {
|
|
619
|
-
this.emit("streamEvent", event);
|
|
620
|
-
switch (event.type) {
|
|
621
|
-
case "thread.started": {
|
|
622
|
-
if (this.sessionInfo) {
|
|
623
|
-
this.sessionInfo.sessionId = event.thread_id;
|
|
170
|
+
get workingDirectory() {
|
|
171
|
+
return self.config.workingDirectory;
|
|
172
|
+
},
|
|
173
|
+
get model() {
|
|
174
|
+
return self.config.model;
|
|
175
|
+
},
|
|
176
|
+
getSessionId: () => self.sessionInfo?.sessionId || "pending",
|
|
177
|
+
getStagedSkillNames: () => self.skillStager.getStagedSkillNames(),
|
|
178
|
+
emitMessage: (message) => self.emit("message", message),
|
|
179
|
+
onThreadStarted: (threadId) => {
|
|
180
|
+
if (self.sessionInfo) {
|
|
181
|
+
self.sessionInfo.sessionId = threadId;
|
|
624
182
|
}
|
|
625
|
-
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
case "item.completed": {
|
|
629
|
-
if (event.item.type === "agent_message") {
|
|
630
|
-
this.emitAssistantMessage(event.item.text);
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
this.emitToolMessagesForItem(event.item, true);
|
|
634
|
-
}
|
|
635
|
-
break;
|
|
636
|
-
}
|
|
637
|
-
case "item.started": {
|
|
638
|
-
this.emitToolMessagesForItem(event.item, false);
|
|
639
|
-
break;
|
|
640
|
-
}
|
|
641
|
-
case "turn.completed": {
|
|
642
|
-
this.lastUsage = parseUsage(event.usage);
|
|
643
|
-
this.pendingResultMessage = this.createSuccessResultMessage(this.lastAssistantText || "Codex session completed successfully");
|
|
644
|
-
break;
|
|
645
|
-
}
|
|
646
|
-
case "turn.failed": {
|
|
647
|
-
// Prefer event.error.message; fallback to last standalone "error" event
|
|
648
|
-
const message = event.error?.message ||
|
|
649
|
-
this.errorMessages.at(-1) ||
|
|
650
|
-
"Codex execution failed";
|
|
651
|
-
this.errorMessages.push(message);
|
|
652
|
-
this.pendingResultMessage = this.createErrorResultMessage(message);
|
|
653
|
-
break;
|
|
654
|
-
}
|
|
655
|
-
case "error": {
|
|
656
|
-
this.errorMessages.push(event.message);
|
|
657
|
-
break;
|
|
658
|
-
}
|
|
659
|
-
default:
|
|
660
|
-
break;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
projectItemToTool(item) {
|
|
664
|
-
switch (item.type) {
|
|
665
|
-
case "command_execution": {
|
|
666
|
-
const commandItem = item;
|
|
667
|
-
const isError = commandItem.status === "failed" ||
|
|
668
|
-
(typeof commandItem.exit_code === "number" &&
|
|
669
|
-
commandItem.exit_code !== 0);
|
|
670
|
-
const result = commandItem.aggregated_output?.trim() ||
|
|
671
|
-
(isError
|
|
672
|
-
? `Command failed (exit code ${commandItem.exit_code ?? "unknown"})`
|
|
673
|
-
: "Command completed with no output");
|
|
674
|
-
return {
|
|
675
|
-
toolUseId: commandItem.id,
|
|
676
|
-
toolName: inferCommandToolName(commandItem.command),
|
|
677
|
-
toolInput: { command: commandItem.command },
|
|
678
|
-
result,
|
|
679
|
-
isError,
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
case "file_change": {
|
|
683
|
-
const fileChangeItem = item;
|
|
684
|
-
const primaryPath = fileChangeItem.changes[0]?.path &&
|
|
685
|
-
normalizeFilePath(fileChangeItem.changes[0].path, this.config.workingDirectory);
|
|
686
|
-
return {
|
|
687
|
-
toolUseId: fileChangeItem.id,
|
|
688
|
-
toolName: "Edit",
|
|
689
|
-
toolInput: {
|
|
690
|
-
...(primaryPath ? { file_path: primaryPath } : {}),
|
|
691
|
-
changes: fileChangeItem.changes.map((change) => ({
|
|
692
|
-
kind: change.kind,
|
|
693
|
-
path: normalizeFilePath(change.path, this.config.workingDirectory),
|
|
694
|
-
})),
|
|
695
|
-
},
|
|
696
|
-
result: summarizeFileChanges(fileChangeItem, this.config.workingDirectory),
|
|
697
|
-
isError: fileChangeItem.status === "failed",
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
case "web_search": {
|
|
701
|
-
const webSearchItem = item;
|
|
702
|
-
const extendedItem = item;
|
|
703
|
-
const action = asRecord(extendedItem.action);
|
|
704
|
-
const actionType = typeof action?.type === "string" ? action.type : undefined;
|
|
705
|
-
const isFetch = actionType === "open_page";
|
|
706
|
-
const url = typeof action?.url === "string"
|
|
707
|
-
? action.url
|
|
708
|
-
: typeof extendedItem.url === "string"
|
|
709
|
-
? extendedItem.url
|
|
710
|
-
: undefined;
|
|
711
|
-
const pattern = typeof action?.pattern === "string"
|
|
712
|
-
? action.pattern
|
|
713
|
-
: typeof extendedItem.pattern === "string"
|
|
714
|
-
? extendedItem.pattern
|
|
715
|
-
: undefined;
|
|
716
|
-
return {
|
|
717
|
-
toolUseId: webSearchItem.id,
|
|
718
|
-
toolName: isFetch ? "WebFetch" : "WebSearch",
|
|
719
|
-
toolInput: isFetch
|
|
720
|
-
? {
|
|
721
|
-
url: url || webSearchItem.query,
|
|
722
|
-
...(pattern ? { pattern } : {}),
|
|
723
|
-
}
|
|
724
|
-
: { query: webSearchItem.query },
|
|
725
|
-
result: action && Object.keys(action).length > 0
|
|
726
|
-
? safeStringify(action)
|
|
727
|
-
: `Search completed for query: ${webSearchItem.query}`,
|
|
728
|
-
isError: false,
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
case "mcp_tool_call": {
|
|
732
|
-
const mcpItem = item;
|
|
733
|
-
return {
|
|
734
|
-
toolUseId: mcpItem.id,
|
|
735
|
-
toolName: `mcp__${normalizeMcpIdentifier(mcpItem.server)}__${normalizeMcpIdentifier(mcpItem.tool)}`,
|
|
736
|
-
toolInput: asRecord(mcpItem.arguments) || {
|
|
737
|
-
arguments: mcpItem.arguments,
|
|
738
|
-
},
|
|
739
|
-
result: toMcpResultString(mcpItem),
|
|
740
|
-
isError: mcpItem.status === "failed" || Boolean(mcpItem.error),
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
case "todo_list": {
|
|
744
|
-
const todoItem = item;
|
|
745
|
-
return {
|
|
746
|
-
toolUseId: todoItem.id,
|
|
747
|
-
toolName: "TodoWrite",
|
|
748
|
-
toolInput: {
|
|
749
|
-
todos: todoItem.items.map((todo) => ({
|
|
750
|
-
content: todo.text,
|
|
751
|
-
status: todo.completed ? "completed" : "pending",
|
|
752
|
-
})),
|
|
753
|
-
},
|
|
754
|
-
result: `Updated todo list (${todoItem.items.length} items)`,
|
|
755
|
-
isError: false,
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
default:
|
|
759
|
-
return null;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
emitToolMessagesForItem(item, includeResult) {
|
|
763
|
-
const projection = this.projectItemToTool(item);
|
|
764
|
-
if (!projection) {
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
if (!this.emittedToolUseIds.has(projection.toolUseId)) {
|
|
768
|
-
const assistantMessage = {
|
|
769
|
-
type: "assistant",
|
|
770
|
-
message: createAssistantToolUseMessage(projection.toolUseId, projection.toolName, projection.toolInput),
|
|
771
|
-
parent_tool_use_id: null,
|
|
772
|
-
uuid: crypto.randomUUID(),
|
|
773
|
-
session_id: this.sessionInfo?.sessionId || "pending",
|
|
774
|
-
};
|
|
775
|
-
this.messages.push(assistantMessage);
|
|
776
|
-
this.emit("message", assistantMessage);
|
|
777
|
-
this.emittedToolUseIds.add(projection.toolUseId);
|
|
778
|
-
}
|
|
779
|
-
if (!includeResult) {
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
const userMessage = {
|
|
783
|
-
type: "user",
|
|
784
|
-
message: createUserToolResultMessage(projection.toolUseId, projection.result, projection.isError),
|
|
785
|
-
parent_tool_use_id: null,
|
|
786
|
-
uuid: crypto.randomUUID(),
|
|
787
|
-
session_id: this.sessionInfo?.sessionId || "pending",
|
|
183
|
+
},
|
|
788
184
|
};
|
|
789
|
-
this.messages.push(userMessage);
|
|
790
|
-
this.emit("message", userMessage);
|
|
791
|
-
this.emittedToolUseIds.delete(projection.toolUseId);
|
|
792
185
|
}
|
|
793
186
|
finalizeSession(caughtError) {
|
|
794
187
|
if (!this.sessionInfo) {
|
|
@@ -796,128 +189,20 @@ export class CodexRunner extends EventEmitter {
|
|
|
796
189
|
return;
|
|
797
190
|
}
|
|
798
191
|
this.sessionInfo.isRunning = false;
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const errorMessage = normalizeError(caughtError);
|
|
805
|
-
this.errorMessages.push(errorMessage);
|
|
806
|
-
}
|
|
807
|
-
if (!this.pendingResultMessage && !this.wasStopped) {
|
|
808
|
-
if (caughtError) {
|
|
809
|
-
this.pendingResultMessage = this.createErrorResultMessage(this.errorMessages.at(-1) || "Codex execution failed");
|
|
810
|
-
}
|
|
811
|
-
else {
|
|
812
|
-
this.pendingResultMessage = this.createSuccessResultMessage(this.lastAssistantText || "Codex session completed successfully");
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
if (this.pendingResultMessage) {
|
|
816
|
-
this.messages.push(this.pendingResultMessage);
|
|
817
|
-
this.emit("message", this.pendingResultMessage);
|
|
818
|
-
this.pendingResultMessage = null;
|
|
819
|
-
}
|
|
820
|
-
this.emit("complete", [...this.messages]);
|
|
192
|
+
const messages = this.mapper.finalize({
|
|
193
|
+
caughtError,
|
|
194
|
+
wasStopped: this.wasStopped,
|
|
195
|
+
});
|
|
196
|
+
this.emit("complete", messages);
|
|
821
197
|
this.cleanupRuntimeState();
|
|
822
198
|
}
|
|
823
|
-
emitAssistantMessage(text) {
|
|
824
|
-
const normalized = text.trim();
|
|
825
|
-
if (!normalized) {
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
this.lastAssistantText = normalized;
|
|
829
|
-
const assistantMessage = {
|
|
830
|
-
type: "assistant",
|
|
831
|
-
message: createAssistantBetaMessage(normalized),
|
|
832
|
-
parent_tool_use_id: null,
|
|
833
|
-
uuid: crypto.randomUUID(),
|
|
834
|
-
session_id: this.sessionInfo?.sessionId || "pending",
|
|
835
|
-
};
|
|
836
|
-
this.messages.push(assistantMessage);
|
|
837
|
-
this.emit("message", assistantMessage);
|
|
838
|
-
}
|
|
839
|
-
emitSystemInitMessage(sessionId) {
|
|
840
|
-
if (this.hasInitMessage) {
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
this.hasInitMessage = true;
|
|
844
|
-
const initMessage = {
|
|
845
|
-
type: "system",
|
|
846
|
-
subtype: "init",
|
|
847
|
-
agents: undefined,
|
|
848
|
-
apiKeySource: "user",
|
|
849
|
-
claude_code_version: "codex-cli",
|
|
850
|
-
cwd: this.config.workingDirectory || cwd(),
|
|
851
|
-
tools: [],
|
|
852
|
-
mcp_servers: [],
|
|
853
|
-
model: this.config.model || DEFAULT_CODEX_MODEL,
|
|
854
|
-
permissionMode: "default",
|
|
855
|
-
slash_commands: [],
|
|
856
|
-
output_style: "default",
|
|
857
|
-
skills: [],
|
|
858
|
-
plugins: [],
|
|
859
|
-
uuid: crypto.randomUUID(),
|
|
860
|
-
session_id: sessionId,
|
|
861
|
-
};
|
|
862
|
-
this.messages.push(initMessage);
|
|
863
|
-
this.emit("message", initMessage);
|
|
864
|
-
}
|
|
865
|
-
createSuccessResultMessage(result) {
|
|
866
|
-
const durationMs = Math.max(Date.now() - this.startTimestampMs, 0);
|
|
867
|
-
return {
|
|
868
|
-
type: "result",
|
|
869
|
-
subtype: "success",
|
|
870
|
-
duration_ms: durationMs,
|
|
871
|
-
duration_api_ms: 0,
|
|
872
|
-
is_error: false,
|
|
873
|
-
num_turns: 1,
|
|
874
|
-
result,
|
|
875
|
-
stop_reason: null,
|
|
876
|
-
total_cost_usd: 0,
|
|
877
|
-
usage: createResultUsage(this.lastUsage),
|
|
878
|
-
modelUsage: {},
|
|
879
|
-
permission_denials: [],
|
|
880
|
-
uuid: crypto.randomUUID(),
|
|
881
|
-
session_id: this.sessionInfo?.sessionId || "pending",
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
createErrorResultMessage(errorMessage) {
|
|
885
|
-
const durationMs = Math.max(Date.now() - this.startTimestampMs, 0);
|
|
886
|
-
return {
|
|
887
|
-
type: "result",
|
|
888
|
-
subtype: "error_during_execution",
|
|
889
|
-
duration_ms: durationMs,
|
|
890
|
-
duration_api_ms: 0,
|
|
891
|
-
is_error: true,
|
|
892
|
-
num_turns: 1,
|
|
893
|
-
stop_reason: null,
|
|
894
|
-
errors: [errorMessage],
|
|
895
|
-
total_cost_usd: 0,
|
|
896
|
-
usage: createResultUsage(this.lastUsage),
|
|
897
|
-
modelUsage: {},
|
|
898
|
-
permission_denials: [],
|
|
899
|
-
uuid: crypto.randomUUID(),
|
|
900
|
-
session_id: this.sessionInfo?.sessionId || "pending",
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
199
|
cleanupRuntimeState() {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
return;
|
|
200
|
+
const backend = this.backend;
|
|
201
|
+
this.backend = null;
|
|
202
|
+
if (backend) {
|
|
203
|
+
void backend.close();
|
|
909
204
|
}
|
|
910
|
-
this.
|
|
911
|
-
this.abortController?.abort();
|
|
912
|
-
}
|
|
913
|
-
isRunning() {
|
|
914
|
-
return this.sessionInfo?.isRunning ?? false;
|
|
915
|
-
}
|
|
916
|
-
getMessages() {
|
|
917
|
-
return [...this.messages];
|
|
918
|
-
}
|
|
919
|
-
getFormatter() {
|
|
920
|
-
return this.formatter;
|
|
205
|
+
this.skillStager.cleanup();
|
|
921
206
|
}
|
|
922
207
|
}
|
|
923
208
|
//# sourceMappingURL=CodexRunner.js.map
|