@engineeros/connector 0.11.2 → 0.12.2
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/bin/engineeros-connector.mjs +791 -790
- package/package.json +38 -37
- package/src/acp-client.mjs +63 -41
- package/src/agent-harness.mjs +51 -51
- package/src/capabilities.mjs +9 -9
- package/src/codex-app-server.mjs +235 -207
- package/src/runner.mjs +1661 -1654
package/src/codex-app-server.mjs
CHANGED
|
@@ -1,218 +1,246 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import readline from "node:readline";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
lines.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
child.stderr.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
pending.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
child.once("
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
let
|
|
103
|
-
let
|
|
104
|
-
let
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import { normalizeTokenUsage } from "./acp-client.mjs";
|
|
4
|
+
|
|
5
|
+
export async function inspectCodexExecutionProfiles({
|
|
6
|
+
workspace,
|
|
7
|
+
command = process.env.CODEX_BIN || (process.platform === "win32" ? "codex.cmd" : "codex"),
|
|
8
|
+
spawnProcess = spawn,
|
|
9
|
+
timeoutMs = 15_000,
|
|
10
|
+
}) {
|
|
11
|
+
const child = spawnProcess(command, ["app-server", "--stdio"], {
|
|
12
|
+
cwd: workspace,
|
|
13
|
+
env: process.env,
|
|
14
|
+
shell: process.platform === "win32",
|
|
15
|
+
windowsHide: true,
|
|
16
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
17
|
+
});
|
|
18
|
+
const pending = new Map();
|
|
19
|
+
let requestId = 0;
|
|
20
|
+
let stderr = "";
|
|
21
|
+
const request = (method, params) => new Promise((resolve, reject) => {
|
|
22
|
+
const id = ++requestId;
|
|
23
|
+
pending.set(id, { resolve, reject });
|
|
24
|
+
child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
25
|
+
});
|
|
26
|
+
const lines = readline.createInterface({ input: child.stdout });
|
|
27
|
+
lines.on("line", (line) => {
|
|
28
|
+
try {
|
|
29
|
+
const message = JSON.parse(line);
|
|
30
|
+
const waiter = pending.get(message.id);
|
|
31
|
+
if (!waiter) return;
|
|
32
|
+
pending.delete(message.id);
|
|
33
|
+
if (message.error) waiter.reject(new Error(message.error.message || "Codex model discovery failed."));
|
|
34
|
+
else waiter.resolve(message.result);
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore non-protocol output.
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
child.stderr.setEncoding("utf8");
|
|
40
|
+
child.stderr.on("data", (chunk) => { stderr = `${stderr}${chunk}`.slice(-4_000); });
|
|
41
|
+
const rejectPending = (message) => {
|
|
42
|
+
for (const waiter of pending.values()) waiter.reject(new Error(message));
|
|
43
|
+
pending.clear();
|
|
44
|
+
};
|
|
45
|
+
child.once("error", (error) => rejectPending(error.message));
|
|
46
|
+
child.once("close", (code) => {
|
|
47
|
+
if (pending.size) rejectPending(`Codex model discovery stopped with code ${code ?? 1}. ${stderr}`.trim());
|
|
48
|
+
});
|
|
49
|
+
const timeout = setTimeout(() => {
|
|
50
|
+
rejectPending("Codex model discovery timed out.");
|
|
51
|
+
child.kill();
|
|
52
|
+
}, timeoutMs);
|
|
53
|
+
try {
|
|
54
|
+
await request("initialize", {
|
|
55
|
+
clientInfo: { name: "engineeros-connector", title: "EngineerOS Connector", version: "0.11.0" },
|
|
56
|
+
});
|
|
57
|
+
child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
|
|
58
|
+
const models = [];
|
|
59
|
+
let cursor = null;
|
|
60
|
+
do {
|
|
61
|
+
const result = await request("model/list", { cursor, includeHidden: false });
|
|
62
|
+
models.push(...(Array.isArray(result?.data) ? result.data : []));
|
|
63
|
+
cursor = result?.nextCursor || null;
|
|
64
|
+
} while (cursor);
|
|
65
|
+
return models.map((item) => ({
|
|
66
|
+
id: item.model || item.id,
|
|
67
|
+
name: item.displayName || item.model || item.id,
|
|
68
|
+
description: item.description || "",
|
|
69
|
+
is_default: item.isDefault === true,
|
|
70
|
+
default_reasoning_effort: item.defaultReasoningEffort || null,
|
|
71
|
+
reasoning_efforts: (item.supportedReasoningEfforts || [])
|
|
72
|
+
.map((option) => option.reasoningEffort)
|
|
73
|
+
.filter(Boolean),
|
|
74
|
+
})).filter((item) => item.id);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
77
|
+
throw new Error(`${detail}${stderr ? ` ${stderr}` : ""}`.trim());
|
|
78
|
+
} finally {
|
|
79
|
+
clearTimeout(timeout);
|
|
80
|
+
child.kill();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function launchCodexAppServer({
|
|
85
|
+
workspace,
|
|
86
|
+
prompt,
|
|
87
|
+
sandbox,
|
|
88
|
+
profile = {},
|
|
89
|
+
previousSessionId,
|
|
90
|
+
callbacks = {},
|
|
91
|
+
command = process.env.CODEX_BIN || (process.platform === "win32" ? "codex.cmd" : "codex"),
|
|
92
|
+
spawnProcess = spawn,
|
|
93
|
+
}) {
|
|
94
|
+
const child = spawnProcess(command, ["app-server", "--stdio"], {
|
|
95
|
+
cwd: workspace,
|
|
96
|
+
env: process.env,
|
|
97
|
+
shell: process.platform === "win32",
|
|
98
|
+
windowsHide: true,
|
|
99
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
100
|
+
});
|
|
101
|
+
const pending = new Map();
|
|
102
|
+
let requestId = 0;
|
|
103
|
+
let threadId = previousSessionId || "";
|
|
104
|
+
let turnId = "";
|
|
105
|
+
let finalMessage = "";
|
|
105
106
|
let stderr = "";
|
|
107
|
+
let usage = null;
|
|
106
108
|
let settled = false;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
let completionTimer = null;
|
|
110
|
+
|
|
111
|
+
const request = (method, params) =>
|
|
112
|
+
new Promise((resolve, reject) => {
|
|
113
|
+
const id = ++requestId;
|
|
114
|
+
pending.set(id, { resolve, reject });
|
|
115
|
+
child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const completed = new Promise((resolve, reject) => {
|
|
116
119
|
const finish = (error) => {
|
|
117
120
|
if (settled) return;
|
|
118
121
|
settled = true;
|
|
119
|
-
|
|
120
|
-
pending.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
122
|
+
if (completionTimer) clearTimeout(completionTimer);
|
|
123
|
+
for (const waiter of pending.values()) waiter.reject(error || new Error("Codex app-server stopped."));
|
|
124
|
+
pending.clear();
|
|
125
|
+
if (error) reject(error);
|
|
126
|
+
else resolve({
|
|
127
|
+
finalMessage,
|
|
128
|
+
output: stderr.slice(-20_000),
|
|
129
|
+
model: profile.model || "codex-app-server",
|
|
130
|
+
sessionId: threadId,
|
|
131
|
+
usage,
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
child.once("error", finish);
|
|
136
|
+
child.once("close", (code) => {
|
|
137
|
+
if (!settled) finish(new Error(`Codex app-server exited before completing the turn (code ${code ?? 1}). ${stderr}`));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const lines = readline.createInterface({ input: child.stdout });
|
|
141
|
+
lines.on("line", (line) => {
|
|
142
|
+
let message;
|
|
143
|
+
try {
|
|
144
|
+
message = JSON.parse(line);
|
|
145
|
+
} catch {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (message.id !== undefined) {
|
|
149
|
+
const waiter = pending.get(message.id);
|
|
150
|
+
if (!waiter) return;
|
|
151
|
+
pending.delete(message.id);
|
|
152
|
+
if (message.error) waiter.reject(new Error(message.error.message || "Codex app-server request failed."));
|
|
153
|
+
else waiter.resolve(message.result);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const params = message.params || {};
|
|
157
|
+
if (message.method === "item/agentMessage/delta" && typeof params.delta === "string") {
|
|
158
|
+
finalMessage += params.delta;
|
|
159
|
+
callbacks.onEvent?.({ type: "codex.agent_message_delta", delta: params.delta });
|
|
160
|
+
} else if (message.method === "item/started" || message.method === "item/completed") {
|
|
161
|
+
callbacks.onEvent?.({ type: `codex.${message.method}`, item: params.item });
|
|
152
162
|
} else if (message.method === "thread/tokenUsage/updated") {
|
|
163
|
+
usage = normalizeTokenUsage(
|
|
164
|
+
params.last || params.usage || params.tokenUsage?.last || params.tokenUsage || params,
|
|
165
|
+
);
|
|
153
166
|
callbacks.onEvent?.({ type: "codex.usage", update: params });
|
|
167
|
+
if (usage && completionTimer) {
|
|
168
|
+
finish();
|
|
169
|
+
child.kill();
|
|
170
|
+
}
|
|
154
171
|
} else if (message.method === "turn/completed" && params.threadId === threadId) {
|
|
155
172
|
const status = params.turn?.status;
|
|
156
|
-
if (status === "failed")
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
await request("initialize", {
|
|
171
|
-
clientInfo: { name: "engineeros-connector", title: "EngineerOS Connector", version: "0.11.0" },
|
|
172
|
-
});
|
|
173
|
-
child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
|
|
174
|
-
const threadResult = previousSessionId
|
|
175
|
-
? await request("thread/resume", {
|
|
176
|
-
threadId: previousSessionId,
|
|
177
|
-
cwd: workspace,
|
|
178
|
-
sandbox,
|
|
179
|
-
approvalPolicy: "never",
|
|
180
|
-
model: profile.model || null,
|
|
181
|
-
})
|
|
182
|
-
: await request("thread/start", {
|
|
183
|
-
cwd: workspace,
|
|
184
|
-
sandbox,
|
|
185
|
-
approvalPolicy: "never",
|
|
186
|
-
model: profile.model || null,
|
|
187
|
-
ephemeral: false,
|
|
188
|
-
});
|
|
189
|
-
threadId = threadResult.thread.id;
|
|
190
|
-
callbacks.onEvent?.({ type: "thread.started", thread_id: threadId });
|
|
191
|
-
const turnResult = await request("turn/start", {
|
|
192
|
-
threadId,
|
|
193
|
-
input: [{ type: "text", text: prompt }],
|
|
194
|
-
effort: profile.reasoning_effort || null,
|
|
195
|
-
});
|
|
196
|
-
turnId = turnResult.turn.id;
|
|
197
|
-
} catch (error) {
|
|
198
|
-
child.kill();
|
|
199
|
-
finish(error instanceof Error ? error : new Error(String(error)));
|
|
200
|
-
}
|
|
201
|
-
})();
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
child,
|
|
206
|
-
completed,
|
|
207
|
-
cancel: async () => {
|
|
208
|
-
if (threadId && turnId && !settled) {
|
|
209
|
-
try {
|
|
210
|
-
await request("turn/interrupt", { threadId, turnId });
|
|
211
|
-
} catch {
|
|
212
|
-
// Process termination below is the final cancellation boundary.
|
|
173
|
+
if (status === "failed") {
|
|
174
|
+
finish(new Error(params.turn?.error?.message || "Codex turn failed."));
|
|
175
|
+
child.kill();
|
|
176
|
+
} else if (!finalMessage.trim()) {
|
|
177
|
+
finish(new Error("Codex completed without returning a response."));
|
|
178
|
+
child.kill();
|
|
179
|
+
} else if (usage) {
|
|
180
|
+
finish();
|
|
181
|
+
child.kill();
|
|
182
|
+
} else if (!completionTimer) {
|
|
183
|
+
completionTimer = setTimeout(() => {
|
|
184
|
+
finish();
|
|
185
|
+
child.kill();
|
|
186
|
+
}, 250);
|
|
213
187
|
}
|
|
214
188
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
child.stderr.setEncoding("utf8");
|
|
192
|
+
child.stderr.on("data", (chunk) => {
|
|
193
|
+
stderr = `${stderr}${chunk}`.slice(-20_000);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
void (async () => {
|
|
197
|
+
try {
|
|
198
|
+
await request("initialize", {
|
|
199
|
+
clientInfo: { name: "engineeros-connector", title: "EngineerOS Connector", version: "0.11.0" },
|
|
200
|
+
});
|
|
201
|
+
child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
|
|
202
|
+
const threadResult = previousSessionId
|
|
203
|
+
? await request("thread/resume", {
|
|
204
|
+
threadId: previousSessionId,
|
|
205
|
+
cwd: workspace,
|
|
206
|
+
sandbox,
|
|
207
|
+
approvalPolicy: "never",
|
|
208
|
+
model: profile.model || null,
|
|
209
|
+
})
|
|
210
|
+
: await request("thread/start", {
|
|
211
|
+
cwd: workspace,
|
|
212
|
+
sandbox,
|
|
213
|
+
approvalPolicy: "never",
|
|
214
|
+
model: profile.model || null,
|
|
215
|
+
ephemeral: false,
|
|
216
|
+
});
|
|
217
|
+
threadId = threadResult.thread.id;
|
|
218
|
+
callbacks.onEvent?.({ type: "thread.started", thread_id: threadId });
|
|
219
|
+
const turnResult = await request("turn/start", {
|
|
220
|
+
threadId,
|
|
221
|
+
input: [{ type: "text", text: prompt }],
|
|
222
|
+
effort: profile.reasoning_effort || null,
|
|
223
|
+
});
|
|
224
|
+
turnId = turnResult.turn.id;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
child.kill();
|
|
227
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
228
|
+
}
|
|
229
|
+
})();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
child,
|
|
234
|
+
completed,
|
|
235
|
+
cancel: async () => {
|
|
236
|
+
if (threadId && turnId && !settled) {
|
|
237
|
+
try {
|
|
238
|
+
await request("turn/interrupt", { threadId, turnId });
|
|
239
|
+
} catch {
|
|
240
|
+
// Process termination below is the final cancellation boundary.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
child.kill();
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|