@open330/oac 2026.2.5
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/CHANGELOG.md +115 -0
- package/LICENSE +21 -0
- package/README.md +597 -0
- package/dist/budget/index.d.ts +117 -0
- package/dist/budget/index.js +23 -0
- package/dist/budget/index.js.map +1 -0
- package/dist/chunk-4IUL7ECC.js +3152 -0
- package/dist/chunk-4IUL7ECC.js.map +1 -0
- package/dist/chunk-5GAUWC3L.js +469 -0
- package/dist/chunk-5GAUWC3L.js.map +1 -0
- package/dist/chunk-6A37SKAJ.js +58 -0
- package/dist/chunk-6A37SKAJ.js.map +1 -0
- package/dist/chunk-7C7SC4TZ.js +358 -0
- package/dist/chunk-7C7SC4TZ.js.map +1 -0
- package/dist/chunk-CJAJ4MBO.js +475 -0
- package/dist/chunk-CJAJ4MBO.js.map +1 -0
- package/dist/chunk-LQC5DLT7.js +317 -0
- package/dist/chunk-LQC5DLT7.js.map +1 -0
- package/dist/chunk-OTPXGXO7.js +2368 -0
- package/dist/chunk-OTPXGXO7.js.map +1 -0
- package/dist/chunk-QPVNC7S4.js +1833 -0
- package/dist/chunk-QPVNC7S4.js.map +1 -0
- package/dist/cli/cli.d.ts +13 -0
- package/dist/cli/cli.js +16 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +22 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/completion/index.d.ts +91 -0
- package/dist/completion/index.js +587 -0
- package/dist/completion/index.js.map +1 -0
- package/dist/config-DequKoFA.d.ts +1468 -0
- package/dist/core/index.d.ts +64 -0
- package/dist/core/index.js +87 -0
- package/dist/core/index.js.map +1 -0
- package/dist/dashboard/index.d.ts +14 -0
- package/dist/dashboard/index.js +1253 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/discovery/index.d.ts +285 -0
- package/dist/discovery/index.js +50 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/event-bus-KiuR6e3P.d.ts +91 -0
- package/dist/execution/index.d.ts +215 -0
- package/dist/execution/index.js +27 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/repo/index.d.ts +33 -0
- package/dist/repo/index.js +19 -0
- package/dist/repo/index.js.map +1 -0
- package/dist/tracking/index.d.ts +357 -0
- package/dist/tracking/index.js +15 -0
- package/dist/tracking/index.js.map +1 -0
- package/dist/types-CYCwgojB.d.ts +34 -0
- package/dist/types-Ck7IucqK.d.ts +195 -0
- package/docs/config-reference.md +271 -0
- package/docs/multi-agent-support-technical-spec.md +312 -0
- package/package.json +82 -0
|
@@ -0,0 +1,1833 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OacError,
|
|
3
|
+
executionError
|
|
4
|
+
} from "./chunk-7C7SC4TZ.js";
|
|
5
|
+
import {
|
|
6
|
+
isRecord
|
|
7
|
+
} from "./chunk-6A37SKAJ.js";
|
|
8
|
+
|
|
9
|
+
// src/execution/agents/claude-code.adapter.ts
|
|
10
|
+
import { createInterface } from "readline";
|
|
11
|
+
import { execa } from "execa";
|
|
12
|
+
|
|
13
|
+
// src/execution/agents/shared.ts
|
|
14
|
+
var AsyncEventQueue = class {
|
|
15
|
+
values = [];
|
|
16
|
+
resolvers = [];
|
|
17
|
+
done = false;
|
|
18
|
+
pendingError;
|
|
19
|
+
push(value) {
|
|
20
|
+
if (this.done) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const nextResolver = this.resolvers.shift();
|
|
24
|
+
if (nextResolver) {
|
|
25
|
+
nextResolver({ done: false, value });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.values.push(value);
|
|
29
|
+
}
|
|
30
|
+
close() {
|
|
31
|
+
if (this.done) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.done = true;
|
|
35
|
+
this.flush();
|
|
36
|
+
}
|
|
37
|
+
fail(error) {
|
|
38
|
+
this.pendingError = error;
|
|
39
|
+
this.done = true;
|
|
40
|
+
this.flush();
|
|
41
|
+
}
|
|
42
|
+
[Symbol.asyncIterator]() {
|
|
43
|
+
return {
|
|
44
|
+
next: async () => {
|
|
45
|
+
if (this.values.length > 0) {
|
|
46
|
+
const value = this.values.shift();
|
|
47
|
+
if (value === void 0) {
|
|
48
|
+
return { done: true, value: void 0 };
|
|
49
|
+
}
|
|
50
|
+
return { done: false, value };
|
|
51
|
+
}
|
|
52
|
+
if (this.pendingError !== void 0) {
|
|
53
|
+
throw this.pendingError;
|
|
54
|
+
}
|
|
55
|
+
if (this.done) {
|
|
56
|
+
return { done: true, value: void 0 };
|
|
57
|
+
}
|
|
58
|
+
return new Promise((resolve2) => {
|
|
59
|
+
this.resolvers.push(resolve2);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
flush() {
|
|
65
|
+
for (const resolve2 of this.resolvers.splice(0)) {
|
|
66
|
+
resolve2({ done: true, value: void 0 });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
function readNumber(value) {
|
|
71
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
return Math.max(0, Math.floor(value));
|
|
75
|
+
}
|
|
76
|
+
function readString(value) {
|
|
77
|
+
if (typeof value !== "string") {
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/execution/agents/claude-code.adapter.ts
|
|
85
|
+
function parseJsonPayload(line) {
|
|
86
|
+
const trimmed = line.trim();
|
|
87
|
+
if (trimmed.length === 0) {
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
const candidates = [trimmed];
|
|
91
|
+
const start = trimmed.indexOf("{");
|
|
92
|
+
const end = trimmed.lastIndexOf("}");
|
|
93
|
+
if (start >= 0 && end > start) {
|
|
94
|
+
const fragment = trimmed.slice(start, end + 1);
|
|
95
|
+
if (fragment !== trimmed) {
|
|
96
|
+
candidates.push(fragment);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(candidate);
|
|
102
|
+
if (isRecord(parsed)) {
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
function patchTokenState(state, patch) {
|
|
111
|
+
if (patch.inputTokens === void 0 && patch.outputTokens === void 0 && patch.cumulativeTokens === void 0) {
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
state.inputTokens = patch.inputTokens ?? state.inputTokens;
|
|
115
|
+
state.outputTokens = patch.outputTokens ?? state.outputTokens;
|
|
116
|
+
const computedTotal = state.inputTokens + state.outputTokens;
|
|
117
|
+
state.cumulativeTokens = Math.max(
|
|
118
|
+
state.cumulativeTokens,
|
|
119
|
+
patch.cumulativeTokens ?? computedTotal
|
|
120
|
+
);
|
|
121
|
+
return {
|
|
122
|
+
type: "tokens",
|
|
123
|
+
inputTokens: state.inputTokens,
|
|
124
|
+
outputTokens: state.outputTokens,
|
|
125
|
+
cumulativeTokens: state.cumulativeTokens
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function parseTokenPatchFromPayload(payload) {
|
|
129
|
+
const usage = isRecord(payload.usage) ? payload.usage : void 0;
|
|
130
|
+
return {
|
|
131
|
+
inputTokens: readNumber(
|
|
132
|
+
payload.inputTokens ?? payload.input_tokens ?? payload.promptTokens ?? payload.prompt_tokens ?? usage?.inputTokens ?? usage?.input_tokens ?? usage?.promptTokens ?? usage?.prompt_tokens
|
|
133
|
+
),
|
|
134
|
+
outputTokens: readNumber(
|
|
135
|
+
payload.outputTokens ?? payload.output_tokens ?? payload.completionTokens ?? payload.completion_tokens ?? usage?.outputTokens ?? usage?.output_tokens ?? usage?.completionTokens ?? usage?.completion_tokens
|
|
136
|
+
),
|
|
137
|
+
cumulativeTokens: readNumber(
|
|
138
|
+
payload.cumulativeTokens ?? payload.cumulative_tokens ?? payload.totalTokens ?? payload.total_tokens ?? usage?.cumulativeTokens ?? usage?.cumulative_tokens ?? usage?.totalTokens ?? usage?.total_tokens
|
|
139
|
+
)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function parseTokenPatchFromLine(line) {
|
|
143
|
+
const inputMatch = line.match(/(?:input|prompt)\s*tokens?\s*[:=]\s*(\d+)/i);
|
|
144
|
+
const outputMatch = line.match(/(?:output|completion)\s*tokens?\s*[:=]\s*(\d+)/i);
|
|
145
|
+
const totalMatch = line.match(/(?:total|cumulative|used)\s*tokens?\s*[:=]\s*(\d+)/i);
|
|
146
|
+
return {
|
|
147
|
+
inputTokens: inputMatch ? Number.parseInt(inputMatch[1], 10) : void 0,
|
|
148
|
+
outputTokens: outputMatch ? Number.parseInt(outputMatch[1], 10) : void 0,
|
|
149
|
+
cumulativeTokens: totalMatch ? Number.parseInt(totalMatch[1], 10) : void 0
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function parseTokenEvent(line, state) {
|
|
153
|
+
const payload = parseJsonPayload(line);
|
|
154
|
+
const patch = payload ? parseTokenPatchFromPayload(payload) : parseTokenPatchFromLine(line);
|
|
155
|
+
return patchTokenState(state, patch);
|
|
156
|
+
}
|
|
157
|
+
function normalizeFileAction(value) {
|
|
158
|
+
if (value !== "create" && value !== "modify" && value !== "delete") {
|
|
159
|
+
return void 0;
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
function parseFileEditFromPayload(payload) {
|
|
164
|
+
if (payload.type === "file_edit") {
|
|
165
|
+
const action = normalizeFileAction(payload.action);
|
|
166
|
+
const path = readString(payload.path);
|
|
167
|
+
if (action && path) {
|
|
168
|
+
return {
|
|
169
|
+
type: "file_edit",
|
|
170
|
+
action,
|
|
171
|
+
path
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const tool = readString(payload.tool ?? payload.tool_name ?? payload.name);
|
|
176
|
+
const input = isRecord(payload.input) ? payload.input : void 0;
|
|
177
|
+
const inputPath = readString(input?.path ?? input?.file_path ?? input?.filePath);
|
|
178
|
+
if (!tool || !inputPath) {
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
if (tool === "create_file") {
|
|
182
|
+
return { type: "file_edit", action: "create", path: inputPath };
|
|
183
|
+
}
|
|
184
|
+
if (tool === "delete_file") {
|
|
185
|
+
return { type: "file_edit", action: "delete", path: inputPath };
|
|
186
|
+
}
|
|
187
|
+
if (tool === "write_file" || tool === "edit_file" || tool === "replace_file") {
|
|
188
|
+
return { type: "file_edit", action: "modify", path: inputPath };
|
|
189
|
+
}
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
function parseFileEditFromLine(line) {
|
|
193
|
+
const fileActionMatch = line.match(/\b(created|modified|deleted)\s+(?:file\s+)?([^\s"'`]+)/i);
|
|
194
|
+
if (!fileActionMatch) {
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
const actionMap = {
|
|
198
|
+
created: "create",
|
|
199
|
+
modified: "modify",
|
|
200
|
+
deleted: "delete"
|
|
201
|
+
};
|
|
202
|
+
const action = actionMap[fileActionMatch[1].toLowerCase()];
|
|
203
|
+
const path = fileActionMatch[2]?.trim();
|
|
204
|
+
if (!action || !path) {
|
|
205
|
+
return void 0;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
type: "file_edit",
|
|
209
|
+
action,
|
|
210
|
+
path
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function parseFileEditEvent(line) {
|
|
214
|
+
const payload = parseJsonPayload(line);
|
|
215
|
+
return payload ? parseFileEditFromPayload(payload) : parseFileEditFromLine(line);
|
|
216
|
+
}
|
|
217
|
+
function parseToolUseEvent(line) {
|
|
218
|
+
const payload = parseJsonPayload(line);
|
|
219
|
+
if (!payload) {
|
|
220
|
+
return void 0;
|
|
221
|
+
}
|
|
222
|
+
const tool = readString(payload.tool ?? payload.tool_name ?? payload.name);
|
|
223
|
+
if (!tool) {
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
type: "tool_use",
|
|
228
|
+
tool,
|
|
229
|
+
input: payload.input
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function parseErrorEvent(line, stream) {
|
|
233
|
+
const payload = parseJsonPayload(line);
|
|
234
|
+
if (payload?.type === "error") {
|
|
235
|
+
const message = readString(payload.message) ?? "Unknown Claude CLI error";
|
|
236
|
+
const recoverable = payload.recoverable !== false;
|
|
237
|
+
return { type: "error", message, recoverable };
|
|
238
|
+
}
|
|
239
|
+
if (stream === "stderr" && /error|failed|exception/i.test(line)) {
|
|
240
|
+
return { type: "error", message: line.trim(), recoverable: true };
|
|
241
|
+
}
|
|
242
|
+
return void 0;
|
|
243
|
+
}
|
|
244
|
+
function estimateTokenCount(text) {
|
|
245
|
+
if (text.length === 0) {
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
249
|
+
}
|
|
250
|
+
function normalizeUnknownError(error, executionId) {
|
|
251
|
+
if (error instanceof OacError) {
|
|
252
|
+
return error;
|
|
253
|
+
}
|
|
254
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
255
|
+
if (/timed out|timeout/i.test(message)) {
|
|
256
|
+
return executionError("AGENT_TIMEOUT", `Claude execution timed out for ${executionId}`, {
|
|
257
|
+
context: { executionId, message },
|
|
258
|
+
cause: error
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (/out of memory|ENOMEM|heap/i.test(message)) {
|
|
262
|
+
return executionError("AGENT_OOM", `Claude execution ran out of memory for ${executionId}`, {
|
|
263
|
+
context: { executionId, message },
|
|
264
|
+
cause: error
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (/network|ECONN|ENOTFOUND|EAI_AGAIN/i.test(message)) {
|
|
268
|
+
return new OacError(
|
|
269
|
+
"Claude execution failed due to network issues",
|
|
270
|
+
"NETWORK_ERROR",
|
|
271
|
+
"recoverable",
|
|
272
|
+
{
|
|
273
|
+
executionId,
|
|
274
|
+
message
|
|
275
|
+
},
|
|
276
|
+
error
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return executionError("AGENT_EXECUTION_FAILED", `Claude execution failed for ${executionId}`, {
|
|
280
|
+
context: { executionId, message },
|
|
281
|
+
cause: error
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function computeTotalTokens(state) {
|
|
285
|
+
return Math.max(state.cumulativeTokens, state.inputTokens + state.outputTokens);
|
|
286
|
+
}
|
|
287
|
+
function normalizeExitCode(value) {
|
|
288
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
function hasBooleanFlag(value, key) {
|
|
294
|
+
if (!isRecord(value)) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
return value[key] === true;
|
|
298
|
+
}
|
|
299
|
+
function buildFailureMessage(stdout, stderr) {
|
|
300
|
+
const trimmedStderr = stderr.trim();
|
|
301
|
+
if (trimmedStderr.length > 0) {
|
|
302
|
+
return trimmedStderr;
|
|
303
|
+
}
|
|
304
|
+
const trimmedStdout = stdout.trim();
|
|
305
|
+
if (trimmedStdout.length > 0) {
|
|
306
|
+
return trimmedStdout;
|
|
307
|
+
}
|
|
308
|
+
return "Claude CLI process exited with a non-zero status.";
|
|
309
|
+
}
|
|
310
|
+
var ClaudeCodeAdapter = class {
|
|
311
|
+
id = "claude-code";
|
|
312
|
+
name = "Claude Code";
|
|
313
|
+
runningExecutions = /* @__PURE__ */ new Map();
|
|
314
|
+
async checkAvailability() {
|
|
315
|
+
try {
|
|
316
|
+
const result = await execa("claude", ["--version"], { reject: false });
|
|
317
|
+
const version = result.stdout.trim().split("\n")[0];
|
|
318
|
+
if (result.exitCode === 0) {
|
|
319
|
+
return {
|
|
320
|
+
available: true,
|
|
321
|
+
version: version.length > 0 ? version : void 0
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
available: false,
|
|
326
|
+
error: result.stderr.trim() || `claude --version exited with code ${result.exitCode}`
|
|
327
|
+
};
|
|
328
|
+
} catch (error) {
|
|
329
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
330
|
+
return {
|
|
331
|
+
available: false,
|
|
332
|
+
error: message
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
execute(params) {
|
|
337
|
+
const startedAt = Date.now();
|
|
338
|
+
const filesChanged = /* @__PURE__ */ new Set();
|
|
339
|
+
const tokenState = {
|
|
340
|
+
inputTokens: 0,
|
|
341
|
+
outputTokens: 0,
|
|
342
|
+
cumulativeTokens: 0
|
|
343
|
+
};
|
|
344
|
+
const eventQueue = new AsyncEventQueue();
|
|
345
|
+
const processEnv = {
|
|
346
|
+
...Object.fromEntries(
|
|
347
|
+
Object.entries(process.env).filter(
|
|
348
|
+
(entry) => typeof entry[1] === "string" && // Strip Claude Code session markers to allow spawning a fresh Claude subprocess
|
|
349
|
+
entry[0] !== "CLAUDECODE" && entry[0] !== "CLAUDE_CODE_SESSION"
|
|
350
|
+
)
|
|
351
|
+
),
|
|
352
|
+
...params.env,
|
|
353
|
+
OAC_TOKEN_BUDGET: `${params.tokenBudget}`,
|
|
354
|
+
OAC_ALLOW_COMMITS: `${params.allowCommits}`
|
|
355
|
+
};
|
|
356
|
+
const subprocess = execa("claude", ["-p", params.prompt], {
|
|
357
|
+
cwd: params.workingDirectory,
|
|
358
|
+
env: processEnv,
|
|
359
|
+
extendEnv: false,
|
|
360
|
+
reject: false,
|
|
361
|
+
timeout: params.timeoutMs
|
|
362
|
+
});
|
|
363
|
+
this.runningExecutions.set(params.executionId, subprocess);
|
|
364
|
+
const consumeStream = async (stream, streamName) => {
|
|
365
|
+
if (!stream) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const lineReader = createInterface({
|
|
369
|
+
input: stream,
|
|
370
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
371
|
+
});
|
|
372
|
+
for await (const line of lineReader) {
|
|
373
|
+
eventQueue.push({
|
|
374
|
+
type: "output",
|
|
375
|
+
content: line,
|
|
376
|
+
stream: streamName
|
|
377
|
+
});
|
|
378
|
+
const tokenEvent = parseTokenEvent(line, tokenState);
|
|
379
|
+
if (tokenEvent?.type === "tokens") {
|
|
380
|
+
eventQueue.push(tokenEvent);
|
|
381
|
+
}
|
|
382
|
+
const fileEvent = parseFileEditEvent(line);
|
|
383
|
+
if (fileEvent) {
|
|
384
|
+
filesChanged.add(fileEvent.path);
|
|
385
|
+
eventQueue.push(fileEvent);
|
|
386
|
+
}
|
|
387
|
+
const toolEvent = parseToolUseEvent(line);
|
|
388
|
+
if (toolEvent) {
|
|
389
|
+
eventQueue.push(toolEvent);
|
|
390
|
+
}
|
|
391
|
+
const errorEvent = parseErrorEvent(line, streamName);
|
|
392
|
+
if (errorEvent) {
|
|
393
|
+
eventQueue.push(errorEvent);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
const stdoutDone = consumeStream(subprocess.stdout ?? void 0, "stdout");
|
|
398
|
+
const stderrDone = consumeStream(subprocess.stderr ?? void 0, "stderr");
|
|
399
|
+
const resultPromise = (async () => {
|
|
400
|
+
try {
|
|
401
|
+
const settled = await subprocess;
|
|
402
|
+
await Promise.all([stdoutDone, stderrDone]);
|
|
403
|
+
const timedOut = hasBooleanFlag(settled, "timedOut");
|
|
404
|
+
if (timedOut) {
|
|
405
|
+
const timeoutError = executionError(
|
|
406
|
+
"AGENT_TIMEOUT",
|
|
407
|
+
`Claude execution timed out for ${params.executionId}`,
|
|
408
|
+
{
|
|
409
|
+
context: {
|
|
410
|
+
executionId: params.executionId,
|
|
411
|
+
timeoutMs: params.timeoutMs
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
eventQueue.push({
|
|
416
|
+
type: "error",
|
|
417
|
+
message: timeoutError.message,
|
|
418
|
+
recoverable: true
|
|
419
|
+
});
|
|
420
|
+
throw timeoutError;
|
|
421
|
+
}
|
|
422
|
+
const canceled = hasBooleanFlag(settled, "isCanceled");
|
|
423
|
+
if (canceled) {
|
|
424
|
+
return {
|
|
425
|
+
success: false,
|
|
426
|
+
exitCode: normalizeExitCode(settled.exitCode),
|
|
427
|
+
totalTokensUsed: computeTotalTokens(tokenState),
|
|
428
|
+
filesChanged: [...filesChanged],
|
|
429
|
+
duration: Date.now() - startedAt,
|
|
430
|
+
error: "Claude execution was cancelled."
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const exitCode = normalizeExitCode(settled.exitCode);
|
|
434
|
+
const success = exitCode === 0;
|
|
435
|
+
return {
|
|
436
|
+
success,
|
|
437
|
+
exitCode,
|
|
438
|
+
totalTokensUsed: computeTotalTokens(tokenState),
|
|
439
|
+
filesChanged: [...filesChanged],
|
|
440
|
+
duration: Date.now() - startedAt,
|
|
441
|
+
error: success ? void 0 : buildFailureMessage(settled.stdout, settled.stderr)
|
|
442
|
+
};
|
|
443
|
+
} catch (error) {
|
|
444
|
+
const normalized = normalizeUnknownError(error, params.executionId);
|
|
445
|
+
eventQueue.push({
|
|
446
|
+
type: "error",
|
|
447
|
+
message: normalized.message,
|
|
448
|
+
recoverable: normalized.severity !== "fatal"
|
|
449
|
+
});
|
|
450
|
+
eventQueue.fail(normalized);
|
|
451
|
+
throw normalized;
|
|
452
|
+
} finally {
|
|
453
|
+
this.runningExecutions.delete(params.executionId);
|
|
454
|
+
eventQueue.close();
|
|
455
|
+
}
|
|
456
|
+
})();
|
|
457
|
+
return {
|
|
458
|
+
executionId: params.executionId,
|
|
459
|
+
providerId: this.id,
|
|
460
|
+
events: eventQueue,
|
|
461
|
+
result: resultPromise,
|
|
462
|
+
pid: subprocess.pid
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async estimateTokens(params) {
|
|
466
|
+
const contextTokens = params.contextTokens ?? params.targetFiles.length * 80 + params.targetFiles.join("\n").length;
|
|
467
|
+
const promptTokens = estimateTokenCount(params.prompt);
|
|
468
|
+
const expectedOutputTokens = params.expectedOutputTokens ?? Math.max(128, Math.ceil(promptTokens * 0.6));
|
|
469
|
+
const totalEstimatedTokens = contextTokens + promptTokens + expectedOutputTokens;
|
|
470
|
+
return {
|
|
471
|
+
taskId: params.taskId,
|
|
472
|
+
providerId: this.id,
|
|
473
|
+
contextTokens,
|
|
474
|
+
promptTokens,
|
|
475
|
+
expectedOutputTokens,
|
|
476
|
+
totalEstimatedTokens,
|
|
477
|
+
confidence: 0.6,
|
|
478
|
+
feasible: true
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
async abort(executionId) {
|
|
482
|
+
const running = this.runningExecutions.get(executionId);
|
|
483
|
+
if (!running) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
running.kill("SIGTERM");
|
|
487
|
+
const forceKillTimer = setTimeout(() => {
|
|
488
|
+
running.kill("SIGKILL");
|
|
489
|
+
}, 5e3);
|
|
490
|
+
forceKillTimer.unref();
|
|
491
|
+
try {
|
|
492
|
+
await running;
|
|
493
|
+
} catch {
|
|
494
|
+
} finally {
|
|
495
|
+
clearTimeout(forceKillTimer);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// src/execution/agents/codex.adapter.ts
|
|
501
|
+
import { stat } from "fs/promises";
|
|
502
|
+
import { createInterface as createInterface2 } from "readline";
|
|
503
|
+
import { execa as execa2 } from "execa";
|
|
504
|
+
function parseJsonPayload2(line) {
|
|
505
|
+
const trimmed = line.trim();
|
|
506
|
+
if (trimmed.length === 0) {
|
|
507
|
+
return void 0;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const parsed = JSON.parse(trimmed);
|
|
511
|
+
if (isRecord(parsed)) {
|
|
512
|
+
return parsed;
|
|
513
|
+
}
|
|
514
|
+
} catch {
|
|
515
|
+
}
|
|
516
|
+
return void 0;
|
|
517
|
+
}
|
|
518
|
+
function parseTokenPatchFromPayload2(payload) {
|
|
519
|
+
const usage = isRecord(payload.usage) ? payload.usage : void 0;
|
|
520
|
+
return {
|
|
521
|
+
inputTokens: readNumber(
|
|
522
|
+
payload.inputTokens ?? payload.input_tokens ?? payload.promptTokens ?? payload.prompt_tokens ?? usage?.inputTokens ?? usage?.input_tokens ?? usage?.promptTokens ?? usage?.prompt_tokens
|
|
523
|
+
),
|
|
524
|
+
outputTokens: readNumber(
|
|
525
|
+
payload.outputTokens ?? payload.output_tokens ?? payload.completionTokens ?? payload.completion_tokens ?? usage?.outputTokens ?? usage?.output_tokens ?? usage?.completionTokens ?? usage?.completion_tokens
|
|
526
|
+
),
|
|
527
|
+
cumulativeTokens: readNumber(
|
|
528
|
+
payload.cumulativeTokens ?? payload.cumulative_tokens ?? payload.totalTokens ?? payload.total_tokens ?? usage?.cumulativeTokens ?? usage?.cumulative_tokens ?? usage?.totalTokens ?? usage?.total_tokens
|
|
529
|
+
)
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function patchTokenState2(state, patch) {
|
|
533
|
+
if (patch.inputTokens === void 0 && patch.outputTokens === void 0 && patch.cumulativeTokens === void 0) {
|
|
534
|
+
return void 0;
|
|
535
|
+
}
|
|
536
|
+
state.inputTokens = patch.inputTokens ?? state.inputTokens;
|
|
537
|
+
state.outputTokens = patch.outputTokens ?? state.outputTokens;
|
|
538
|
+
const computedTotal = state.inputTokens + state.outputTokens;
|
|
539
|
+
state.cumulativeTokens = Math.max(
|
|
540
|
+
state.cumulativeTokens,
|
|
541
|
+
patch.cumulativeTokens ?? computedTotal
|
|
542
|
+
);
|
|
543
|
+
return {
|
|
544
|
+
type: "tokens",
|
|
545
|
+
inputTokens: state.inputTokens,
|
|
546
|
+
outputTokens: state.outputTokens,
|
|
547
|
+
cumulativeTokens: state.cumulativeTokens
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function parseTokenEvent2(line, payload, state) {
|
|
551
|
+
if (payload) {
|
|
552
|
+
return patchTokenState2(state, parseTokenPatchFromPayload2(payload));
|
|
553
|
+
}
|
|
554
|
+
const inputMatch = line.match(/(?:input|prompt)\s*tokens?\s*[:=]\s*(\d+)/i);
|
|
555
|
+
const outputMatch = line.match(/(?:output|completion)\s*tokens?\s*[:=]\s*(\d+)/i);
|
|
556
|
+
const totalMatch = line.match(/(?:total|cumulative|used)\s*tokens?\s*[:=]\s*(\d+)/i);
|
|
557
|
+
return patchTokenState2(state, {
|
|
558
|
+
inputTokens: inputMatch ? Number.parseInt(inputMatch[1], 10) : void 0,
|
|
559
|
+
outputTokens: outputMatch ? Number.parseInt(outputMatch[1], 10) : void 0,
|
|
560
|
+
cumulativeTokens: totalMatch ? Number.parseInt(totalMatch[1], 10) : void 0
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
function normalizeFileAction2(value) {
|
|
564
|
+
if (value !== "create" && value !== "modify" && value !== "delete") {
|
|
565
|
+
return void 0;
|
|
566
|
+
}
|
|
567
|
+
return value;
|
|
568
|
+
}
|
|
569
|
+
function parseFileEditFromPayload2(payload) {
|
|
570
|
+
if (payload.type === "file_edit") {
|
|
571
|
+
const action = normalizeFileAction2(payload.action);
|
|
572
|
+
const path = readString(payload.path);
|
|
573
|
+
if (action && path) {
|
|
574
|
+
return {
|
|
575
|
+
type: "file_edit",
|
|
576
|
+
action,
|
|
577
|
+
path
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const tool = readString(payload.tool ?? payload.tool_name ?? payload.name);
|
|
582
|
+
const input = isRecord(payload.input) ? payload.input : void 0;
|
|
583
|
+
const inputPath = readString(input?.path ?? input?.file_path ?? input?.filePath);
|
|
584
|
+
if (!tool || !inputPath) {
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
587
|
+
if (tool === "create_file") {
|
|
588
|
+
return { type: "file_edit", action: "create", path: inputPath };
|
|
589
|
+
}
|
|
590
|
+
if (tool === "delete_file") {
|
|
591
|
+
return { type: "file_edit", action: "delete", path: inputPath };
|
|
592
|
+
}
|
|
593
|
+
if (tool === "write_file" || tool === "edit_file" || tool === "replace_file") {
|
|
594
|
+
return { type: "file_edit", action: "modify", path: inputPath };
|
|
595
|
+
}
|
|
596
|
+
return void 0;
|
|
597
|
+
}
|
|
598
|
+
function parseToolUseFromPayload(payload) {
|
|
599
|
+
const tool = readString(payload.tool ?? payload.tool_name ?? payload.name);
|
|
600
|
+
if (!tool) {
|
|
601
|
+
return void 0;
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
type: "tool_use",
|
|
605
|
+
tool,
|
|
606
|
+
input: payload.input
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function parseErrorFromPayload(payload) {
|
|
610
|
+
if (payload.type !== "error") {
|
|
611
|
+
return void 0;
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
type: "error",
|
|
615
|
+
message: readString(payload.message) ?? "Unknown Codex CLI error",
|
|
616
|
+
recoverable: payload.recoverable !== false
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function estimateTokenCount2(text) {
|
|
620
|
+
if (text.length === 0) {
|
|
621
|
+
return 0;
|
|
622
|
+
}
|
|
623
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
624
|
+
}
|
|
625
|
+
function normalizeUnknownError2(error, executionId) {
|
|
626
|
+
if (error instanceof OacError) {
|
|
627
|
+
return error;
|
|
628
|
+
}
|
|
629
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
630
|
+
if (/timed out|timeout/i.test(message)) {
|
|
631
|
+
return executionError("AGENT_TIMEOUT", `Codex execution timed out for ${executionId}`, {
|
|
632
|
+
context: { executionId, message },
|
|
633
|
+
cause: error
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (/out of memory|ENOMEM|heap/i.test(message)) {
|
|
637
|
+
return executionError("AGENT_OOM", `Codex execution ran out of memory for ${executionId}`, {
|
|
638
|
+
context: { executionId, message },
|
|
639
|
+
cause: error
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (/rate.limit|429|too many requests|throttl/i.test(message)) {
|
|
643
|
+
return executionError("AGENT_RATE_LIMITED", `Codex execution rate-limited for ${executionId}`, {
|
|
644
|
+
context: { executionId, message },
|
|
645
|
+
cause: error
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (/network|ECONN|ENOTFOUND|EAI_AGAIN/i.test(message)) {
|
|
649
|
+
return new OacError(
|
|
650
|
+
"Codex execution failed due to network issues",
|
|
651
|
+
"NETWORK_ERROR",
|
|
652
|
+
"recoverable",
|
|
653
|
+
{
|
|
654
|
+
executionId,
|
|
655
|
+
message
|
|
656
|
+
},
|
|
657
|
+
error
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
return executionError("AGENT_EXECUTION_FAILED", `Codex execution failed for ${executionId}`, {
|
|
661
|
+
context: { executionId, message },
|
|
662
|
+
cause: error
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function computeTotalTokens2(state) {
|
|
666
|
+
return Math.max(state.cumulativeTokens, state.inputTokens + state.outputTokens);
|
|
667
|
+
}
|
|
668
|
+
function normalizeExitCode2(value) {
|
|
669
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
670
|
+
return value;
|
|
671
|
+
}
|
|
672
|
+
return 1;
|
|
673
|
+
}
|
|
674
|
+
function hasBooleanFlag2(value, key) {
|
|
675
|
+
if (!isRecord(value)) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
return value[key] === true;
|
|
679
|
+
}
|
|
680
|
+
function buildFailureMessage2(stdout, stderr) {
|
|
681
|
+
const trimmedStderr = stderr.trim();
|
|
682
|
+
if (trimmedStderr.length > 0) {
|
|
683
|
+
return trimmedStderr;
|
|
684
|
+
}
|
|
685
|
+
const trimmedStdout = stdout.trim();
|
|
686
|
+
if (trimmedStdout.length > 0) {
|
|
687
|
+
return trimmedStdout;
|
|
688
|
+
}
|
|
689
|
+
return "Codex CLI process exited with a non-zero status.";
|
|
690
|
+
}
|
|
691
|
+
function parseVersion(output) {
|
|
692
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
693
|
+
if (!match) {
|
|
694
|
+
return void 0;
|
|
695
|
+
}
|
|
696
|
+
return match[1];
|
|
697
|
+
}
|
|
698
|
+
async function estimateContextTokens(targetFiles) {
|
|
699
|
+
let totalBytes = 0;
|
|
700
|
+
for (const filePath of targetFiles) {
|
|
701
|
+
try {
|
|
702
|
+
const fileStat = await stat(filePath);
|
|
703
|
+
if (fileStat.isFile()) {
|
|
704
|
+
totalBytes += fileStat.size;
|
|
705
|
+
}
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return Math.ceil(totalBytes / 4);
|
|
710
|
+
}
|
|
711
|
+
var CodexAdapter = class {
|
|
712
|
+
id = "codex";
|
|
713
|
+
name = "Codex CLI";
|
|
714
|
+
runningExecutions = /* @__PURE__ */ new Map();
|
|
715
|
+
async checkAvailability() {
|
|
716
|
+
try {
|
|
717
|
+
const result = await execa2("codex", ["--version"], { reject: false });
|
|
718
|
+
if (result.exitCode === 0) {
|
|
719
|
+
const versionLine = result.stdout.trim().split("\n")[0] ?? "";
|
|
720
|
+
return {
|
|
721
|
+
available: true,
|
|
722
|
+
version: parseVersion(versionLine)
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
available: false,
|
|
727
|
+
error: result.stderr.trim() || result.stdout.trim() || `codex --version exited with code ${result.exitCode}`
|
|
728
|
+
};
|
|
729
|
+
} catch (error) {
|
|
730
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
731
|
+
return {
|
|
732
|
+
available: false,
|
|
733
|
+
error: message
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
execute(params) {
|
|
738
|
+
const startedAt = Date.now();
|
|
739
|
+
const filesChanged = /* @__PURE__ */ new Set();
|
|
740
|
+
const tokenState = {
|
|
741
|
+
inputTokens: 0,
|
|
742
|
+
outputTokens: 0,
|
|
743
|
+
cumulativeTokens: 0
|
|
744
|
+
};
|
|
745
|
+
const eventQueue = new AsyncEventQueue();
|
|
746
|
+
const processEnv = {
|
|
747
|
+
...Object.fromEntries(
|
|
748
|
+
Object.entries(process.env).filter(
|
|
749
|
+
(entry) => typeof entry[1] === "string"
|
|
750
|
+
)
|
|
751
|
+
),
|
|
752
|
+
...params.env,
|
|
753
|
+
OAC_TOKEN_BUDGET: `${params.tokenBudget}`,
|
|
754
|
+
OAC_ALLOW_COMMITS: `${params.allowCommits}`
|
|
755
|
+
};
|
|
756
|
+
const subprocess = execa2(
|
|
757
|
+
"codex",
|
|
758
|
+
["exec", "--full-auto", "-C", params.workingDirectory, params.prompt],
|
|
759
|
+
{
|
|
760
|
+
cwd: params.workingDirectory,
|
|
761
|
+
env: processEnv,
|
|
762
|
+
reject: false,
|
|
763
|
+
timeout: params.timeoutMs
|
|
764
|
+
}
|
|
765
|
+
);
|
|
766
|
+
this.runningExecutions.set(params.executionId, subprocess);
|
|
767
|
+
const processStdoutLine = (line) => {
|
|
768
|
+
const payload = parseJsonPayload2(line);
|
|
769
|
+
const tokenEvent = parseTokenEvent2(line, payload, tokenState);
|
|
770
|
+
if (tokenEvent?.type === "tokens") {
|
|
771
|
+
eventQueue.push(tokenEvent);
|
|
772
|
+
}
|
|
773
|
+
if (!payload) return;
|
|
774
|
+
const fileEvent = parseFileEditFromPayload2(payload);
|
|
775
|
+
if (fileEvent) {
|
|
776
|
+
filesChanged.add(fileEvent.path);
|
|
777
|
+
eventQueue.push(fileEvent);
|
|
778
|
+
}
|
|
779
|
+
const toolEvent = parseToolUseFromPayload(payload);
|
|
780
|
+
if (toolEvent) {
|
|
781
|
+
eventQueue.push(toolEvent);
|
|
782
|
+
}
|
|
783
|
+
const errorEvent = parseErrorFromPayload(payload);
|
|
784
|
+
if (errorEvent) {
|
|
785
|
+
eventQueue.push(errorEvent);
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
const consumeStream = async (stream, streamName) => {
|
|
789
|
+
if (!stream) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const lineReader = createInterface2({
|
|
793
|
+
input: stream,
|
|
794
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
795
|
+
});
|
|
796
|
+
for await (const line of lineReader) {
|
|
797
|
+
eventQueue.push({ type: "output", content: line, stream: streamName });
|
|
798
|
+
if (streamName === "stdout") {
|
|
799
|
+
processStdoutLine(line);
|
|
800
|
+
} else if (/error|failed|exception/i.test(line)) {
|
|
801
|
+
eventQueue.push({ type: "error", message: line.trim(), recoverable: true });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
const stdoutDone = consumeStream(subprocess.stdout ?? void 0, "stdout");
|
|
806
|
+
const stderrDone = consumeStream(subprocess.stderr ?? void 0, "stderr");
|
|
807
|
+
const resultPromise = (async () => {
|
|
808
|
+
try {
|
|
809
|
+
const settled = await subprocess;
|
|
810
|
+
await Promise.all([stdoutDone, stderrDone]);
|
|
811
|
+
const timedOut = hasBooleanFlag2(settled, "timedOut");
|
|
812
|
+
if (timedOut) {
|
|
813
|
+
const timeoutError = executionError(
|
|
814
|
+
"AGENT_TIMEOUT",
|
|
815
|
+
`Codex execution timed out for ${params.executionId}`,
|
|
816
|
+
{
|
|
817
|
+
context: {
|
|
818
|
+
executionId: params.executionId,
|
|
819
|
+
timeoutMs: params.timeoutMs
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
);
|
|
823
|
+
eventQueue.push({
|
|
824
|
+
type: "error",
|
|
825
|
+
message: timeoutError.message,
|
|
826
|
+
recoverable: true
|
|
827
|
+
});
|
|
828
|
+
throw timeoutError;
|
|
829
|
+
}
|
|
830
|
+
const canceled = hasBooleanFlag2(settled, "isCanceled");
|
|
831
|
+
if (canceled) {
|
|
832
|
+
return {
|
|
833
|
+
success: false,
|
|
834
|
+
exitCode: normalizeExitCode2(settled.exitCode),
|
|
835
|
+
totalTokensUsed: computeTotalTokens2(tokenState),
|
|
836
|
+
filesChanged: [...filesChanged],
|
|
837
|
+
duration: Date.now() - startedAt,
|
|
838
|
+
error: "Codex execution was cancelled."
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
const exitCode = normalizeExitCode2(settled.exitCode);
|
|
842
|
+
const success = exitCode === 0;
|
|
843
|
+
return {
|
|
844
|
+
success,
|
|
845
|
+
exitCode,
|
|
846
|
+
totalTokensUsed: computeTotalTokens2(tokenState),
|
|
847
|
+
filesChanged: [...filesChanged],
|
|
848
|
+
duration: Date.now() - startedAt,
|
|
849
|
+
error: success ? void 0 : buildFailureMessage2(settled.stdout, settled.stderr)
|
|
850
|
+
};
|
|
851
|
+
} catch (error) {
|
|
852
|
+
const normalized = normalizeUnknownError2(error, params.executionId);
|
|
853
|
+
eventQueue.push({
|
|
854
|
+
type: "error",
|
|
855
|
+
message: normalized.message,
|
|
856
|
+
recoverable: normalized.severity !== "fatal"
|
|
857
|
+
});
|
|
858
|
+
eventQueue.fail(normalized);
|
|
859
|
+
throw normalized;
|
|
860
|
+
} finally {
|
|
861
|
+
this.runningExecutions.delete(params.executionId);
|
|
862
|
+
eventQueue.close();
|
|
863
|
+
}
|
|
864
|
+
})();
|
|
865
|
+
return {
|
|
866
|
+
executionId: params.executionId,
|
|
867
|
+
providerId: this.id,
|
|
868
|
+
events: eventQueue,
|
|
869
|
+
result: resultPromise,
|
|
870
|
+
pid: subprocess.pid
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
async estimateTokens(params) {
|
|
874
|
+
const baseTokens = params.targetFiles.length * 2e3;
|
|
875
|
+
const promptTokens = estimateTokenCount2(params.prompt);
|
|
876
|
+
const contextTokens = await estimateContextTokens(params.targetFiles);
|
|
877
|
+
const expectedOutputTokens = baseTokens;
|
|
878
|
+
const totalEstimatedTokens = contextTokens + promptTokens + expectedOutputTokens;
|
|
879
|
+
return {
|
|
880
|
+
taskId: params.taskId,
|
|
881
|
+
providerId: this.id,
|
|
882
|
+
contextTokens,
|
|
883
|
+
promptTokens,
|
|
884
|
+
expectedOutputTokens,
|
|
885
|
+
totalEstimatedTokens,
|
|
886
|
+
confidence: 0.6,
|
|
887
|
+
feasible: totalEstimatedTokens < 2e5
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
async abort(executionId) {
|
|
891
|
+
const running = this.runningExecutions.get(executionId);
|
|
892
|
+
if (!running) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
running.kill("SIGTERM");
|
|
896
|
+
const forceKillTimer = setTimeout(() => {
|
|
897
|
+
running.kill("SIGKILL");
|
|
898
|
+
}, 2e3);
|
|
899
|
+
forceKillTimer.unref();
|
|
900
|
+
try {
|
|
901
|
+
await running;
|
|
902
|
+
} catch {
|
|
903
|
+
} finally {
|
|
904
|
+
clearTimeout(forceKillTimer);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
// src/execution/agents/opencode.adapter.ts
|
|
910
|
+
import { stat as stat2 } from "fs/promises";
|
|
911
|
+
import { createInterface as createInterface3 } from "readline";
|
|
912
|
+
import { execa as execa3 } from "execa";
|
|
913
|
+
function parseJsonPayload3(line) {
|
|
914
|
+
const trimmed = line.trim();
|
|
915
|
+
if (trimmed.length === 0) return void 0;
|
|
916
|
+
try {
|
|
917
|
+
const parsed = JSON.parse(trimmed);
|
|
918
|
+
if (isRecord(parsed)) return parsed;
|
|
919
|
+
} catch {
|
|
920
|
+
}
|
|
921
|
+
return void 0;
|
|
922
|
+
}
|
|
923
|
+
function parseTokenPatchFromPayload3(payload) {
|
|
924
|
+
const usage = isRecord(payload.usage) ? payload.usage : void 0;
|
|
925
|
+
return {
|
|
926
|
+
inputTokens: readNumber(
|
|
927
|
+
payload.inputTokens ?? payload.input_tokens ?? payload.promptTokens ?? payload.prompt_tokens ?? usage?.inputTokens ?? usage?.input_tokens ?? usage?.promptTokens ?? usage?.prompt_tokens
|
|
928
|
+
),
|
|
929
|
+
outputTokens: readNumber(
|
|
930
|
+
payload.outputTokens ?? payload.output_tokens ?? payload.completionTokens ?? payload.completion_tokens ?? usage?.outputTokens ?? usage?.output_tokens ?? usage?.completionTokens ?? usage?.completion_tokens
|
|
931
|
+
),
|
|
932
|
+
cumulativeTokens: readNumber(
|
|
933
|
+
payload.cumulativeTokens ?? payload.cumulative_tokens ?? payload.totalTokens ?? payload.total_tokens ?? usage?.cumulativeTokens ?? usage?.cumulative_tokens ?? usage?.totalTokens ?? usage?.total_tokens
|
|
934
|
+
)
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function patchTokenState3(state, patch) {
|
|
938
|
+
if (patch.inputTokens === void 0 && patch.outputTokens === void 0 && patch.cumulativeTokens === void 0) {
|
|
939
|
+
return void 0;
|
|
940
|
+
}
|
|
941
|
+
state.inputTokens = patch.inputTokens ?? state.inputTokens;
|
|
942
|
+
state.outputTokens = patch.outputTokens ?? state.outputTokens;
|
|
943
|
+
const computedTotal = state.inputTokens + state.outputTokens;
|
|
944
|
+
state.cumulativeTokens = Math.max(
|
|
945
|
+
state.cumulativeTokens,
|
|
946
|
+
patch.cumulativeTokens ?? computedTotal
|
|
947
|
+
);
|
|
948
|
+
return {
|
|
949
|
+
type: "tokens",
|
|
950
|
+
inputTokens: state.inputTokens,
|
|
951
|
+
outputTokens: state.outputTokens,
|
|
952
|
+
cumulativeTokens: state.cumulativeTokens
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function parseTokenEvent3(line, payload, state) {
|
|
956
|
+
if (payload) {
|
|
957
|
+
return patchTokenState3(state, parseTokenPatchFromPayload3(payload));
|
|
958
|
+
}
|
|
959
|
+
const inputMatch = line.match(/(?:input|prompt)\s*tokens?\s*[:=]\s*(\d+)/i);
|
|
960
|
+
const outputMatch = line.match(/(?:output|completion)\s*tokens?\s*[:=]\s*(\d+)/i);
|
|
961
|
+
const totalMatch = line.match(/(?:total|cumulative|used)\s*tokens?\s*[:=]\s*(\d+)/i);
|
|
962
|
+
return patchTokenState3(state, {
|
|
963
|
+
inputTokens: inputMatch ? Number.parseInt(inputMatch[1], 10) : void 0,
|
|
964
|
+
outputTokens: outputMatch ? Number.parseInt(outputMatch[1], 10) : void 0,
|
|
965
|
+
cumulativeTokens: totalMatch ? Number.parseInt(totalMatch[1], 10) : void 0
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
function normalizeFileAction3(value) {
|
|
969
|
+
if (value !== "create" && value !== "modify" && value !== "delete") return void 0;
|
|
970
|
+
return value;
|
|
971
|
+
}
|
|
972
|
+
function parseFileEditFromPayload3(payload) {
|
|
973
|
+
if (payload.type === "file_edit") {
|
|
974
|
+
const action = normalizeFileAction3(payload.action);
|
|
975
|
+
const path = readString(payload.path);
|
|
976
|
+
if (action && path) return { type: "file_edit", action, path };
|
|
977
|
+
}
|
|
978
|
+
const tool = readString(payload.tool ?? payload.tool_name ?? payload.name);
|
|
979
|
+
const input = isRecord(payload.input) ? payload.input : void 0;
|
|
980
|
+
const inputPath = readString(input?.path ?? input?.file_path ?? input?.filePath);
|
|
981
|
+
if (!tool || !inputPath) return void 0;
|
|
982
|
+
if (tool === "create_file") return { type: "file_edit", action: "create", path: inputPath };
|
|
983
|
+
if (tool === "delete_file") return { type: "file_edit", action: "delete", path: inputPath };
|
|
984
|
+
if (tool === "write_file" || tool === "edit_file" || tool === "replace_file") {
|
|
985
|
+
return { type: "file_edit", action: "modify", path: inputPath };
|
|
986
|
+
}
|
|
987
|
+
return void 0;
|
|
988
|
+
}
|
|
989
|
+
function parseToolUseFromPayload2(payload) {
|
|
990
|
+
const tool = readString(payload.tool ?? payload.tool_name ?? payload.name);
|
|
991
|
+
if (!tool) return void 0;
|
|
992
|
+
return { type: "tool_use", tool, input: payload.input };
|
|
993
|
+
}
|
|
994
|
+
function parseErrorFromPayload2(payload) {
|
|
995
|
+
if (payload.type !== "error") return void 0;
|
|
996
|
+
return {
|
|
997
|
+
type: "error",
|
|
998
|
+
message: readString(payload.message) ?? "Unknown OpenCode CLI error",
|
|
999
|
+
recoverable: payload.recoverable !== false
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function estimateTokenCount3(text) {
|
|
1003
|
+
if (text.length === 0) return 0;
|
|
1004
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
1005
|
+
}
|
|
1006
|
+
function normalizeUnknownError3(error, executionId) {
|
|
1007
|
+
if (error instanceof OacError) return error;
|
|
1008
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1009
|
+
if (/timed out|timeout/i.test(message)) {
|
|
1010
|
+
return executionError("AGENT_TIMEOUT", `OpenCode execution timed out for ${executionId}`, {
|
|
1011
|
+
context: { executionId, message },
|
|
1012
|
+
cause: error
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
if (/out of memory|ENOMEM|heap/i.test(message)) {
|
|
1016
|
+
return executionError("AGENT_OOM", `OpenCode execution ran out of memory for ${executionId}`, {
|
|
1017
|
+
context: { executionId, message },
|
|
1018
|
+
cause: error
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
if (/rate.limit|429|too many requests|throttl/i.test(message)) {
|
|
1022
|
+
return executionError("AGENT_RATE_LIMITED", `OpenCode execution rate-limited for ${executionId}`, {
|
|
1023
|
+
context: { executionId, message },
|
|
1024
|
+
cause: error
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (/network|ECONN|ENOTFOUND|EAI_AGAIN/i.test(message)) {
|
|
1028
|
+
return new OacError(
|
|
1029
|
+
"OpenCode execution failed due to network issues",
|
|
1030
|
+
"NETWORK_ERROR",
|
|
1031
|
+
"recoverable",
|
|
1032
|
+
{ executionId, message },
|
|
1033
|
+
error
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
return executionError("AGENT_EXECUTION_FAILED", `OpenCode execution failed for ${executionId}`, {
|
|
1037
|
+
context: { executionId, message },
|
|
1038
|
+
cause: error
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
function computeTotalTokens3(state) {
|
|
1042
|
+
return Math.max(state.cumulativeTokens, state.inputTokens + state.outputTokens);
|
|
1043
|
+
}
|
|
1044
|
+
function normalizeExitCode3(value) {
|
|
1045
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1046
|
+
return 1;
|
|
1047
|
+
}
|
|
1048
|
+
function hasBooleanFlag3(value, key) {
|
|
1049
|
+
if (!isRecord(value)) return false;
|
|
1050
|
+
return value[key] === true;
|
|
1051
|
+
}
|
|
1052
|
+
function buildFailureMessage3(stdout, stderr) {
|
|
1053
|
+
const trimmedStderr = stderr.trim();
|
|
1054
|
+
if (trimmedStderr.length > 0) return trimmedStderr;
|
|
1055
|
+
const trimmedStdout = stdout.trim();
|
|
1056
|
+
if (trimmedStdout.length > 0) return trimmedStdout;
|
|
1057
|
+
return "OpenCode CLI process exited with a non-zero status.";
|
|
1058
|
+
}
|
|
1059
|
+
function parseVersion2(output) {
|
|
1060
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
1061
|
+
return match ? match[1] : void 0;
|
|
1062
|
+
}
|
|
1063
|
+
async function estimateContextTokens2(targetFiles) {
|
|
1064
|
+
let totalBytes = 0;
|
|
1065
|
+
for (const filePath of targetFiles) {
|
|
1066
|
+
try {
|
|
1067
|
+
const fileStat = await stat2(filePath);
|
|
1068
|
+
if (fileStat.isFile()) totalBytes += fileStat.size;
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return Math.ceil(totalBytes / 4);
|
|
1073
|
+
}
|
|
1074
|
+
var OpenCodeAdapter = class {
|
|
1075
|
+
id = "opencode";
|
|
1076
|
+
name = "OpenCode";
|
|
1077
|
+
runningExecutions = /* @__PURE__ */ new Map();
|
|
1078
|
+
async checkAvailability() {
|
|
1079
|
+
try {
|
|
1080
|
+
const result = await execa3("opencode", ["--version"], { reject: false });
|
|
1081
|
+
if (result.exitCode === 0) {
|
|
1082
|
+
const versionLine = result.stdout.trim().split("\n")[0] ?? "";
|
|
1083
|
+
return {
|
|
1084
|
+
available: true,
|
|
1085
|
+
version: parseVersion2(versionLine)
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
available: false,
|
|
1090
|
+
error: result.stderr.trim() || result.stdout.trim() || `opencode --version exited with code ${result.exitCode}`
|
|
1091
|
+
};
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1094
|
+
return { available: false, error: message };
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
execute(params) {
|
|
1098
|
+
const startedAt = Date.now();
|
|
1099
|
+
const filesChanged = /* @__PURE__ */ new Set();
|
|
1100
|
+
const tokenState = {
|
|
1101
|
+
inputTokens: 0,
|
|
1102
|
+
outputTokens: 0,
|
|
1103
|
+
cumulativeTokens: 0
|
|
1104
|
+
};
|
|
1105
|
+
const eventQueue = new AsyncEventQueue();
|
|
1106
|
+
const processEnv = {
|
|
1107
|
+
...Object.fromEntries(
|
|
1108
|
+
Object.entries(process.env).filter(
|
|
1109
|
+
(entry) => typeof entry[1] === "string"
|
|
1110
|
+
)
|
|
1111
|
+
),
|
|
1112
|
+
...params.env,
|
|
1113
|
+
OAC_TOKEN_BUDGET: `${params.tokenBudget}`,
|
|
1114
|
+
OAC_ALLOW_COMMITS: `${params.allowCommits}`
|
|
1115
|
+
};
|
|
1116
|
+
const subprocess = execa3(
|
|
1117
|
+
"opencode",
|
|
1118
|
+
["run", "--format", "json", params.prompt],
|
|
1119
|
+
{
|
|
1120
|
+
cwd: params.workingDirectory,
|
|
1121
|
+
env: processEnv,
|
|
1122
|
+
reject: false,
|
|
1123
|
+
timeout: params.timeoutMs
|
|
1124
|
+
}
|
|
1125
|
+
);
|
|
1126
|
+
this.runningExecutions.set(params.executionId, subprocess);
|
|
1127
|
+
const processStdoutLine = (line) => {
|
|
1128
|
+
const payload = parseJsonPayload3(line);
|
|
1129
|
+
const tokenEvent = parseTokenEvent3(line, payload, tokenState);
|
|
1130
|
+
if (tokenEvent?.type === "tokens") {
|
|
1131
|
+
eventQueue.push(tokenEvent);
|
|
1132
|
+
}
|
|
1133
|
+
if (!payload) return;
|
|
1134
|
+
const fileEvent = parseFileEditFromPayload3(payload);
|
|
1135
|
+
if (fileEvent) {
|
|
1136
|
+
filesChanged.add(fileEvent.path);
|
|
1137
|
+
eventQueue.push(fileEvent);
|
|
1138
|
+
}
|
|
1139
|
+
const toolEvent = parseToolUseFromPayload2(payload);
|
|
1140
|
+
if (toolEvent) {
|
|
1141
|
+
eventQueue.push(toolEvent);
|
|
1142
|
+
}
|
|
1143
|
+
const errorEvent = parseErrorFromPayload2(payload);
|
|
1144
|
+
if (errorEvent) {
|
|
1145
|
+
eventQueue.push(errorEvent);
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
const consumeStream = async (stream, streamName) => {
|
|
1149
|
+
if (!stream) return;
|
|
1150
|
+
const lineReader = createInterface3({
|
|
1151
|
+
input: stream,
|
|
1152
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
1153
|
+
});
|
|
1154
|
+
for await (const line of lineReader) {
|
|
1155
|
+
eventQueue.push({ type: "output", content: line, stream: streamName });
|
|
1156
|
+
if (streamName === "stdout") {
|
|
1157
|
+
processStdoutLine(line);
|
|
1158
|
+
} else if (/error|failed|exception/i.test(line)) {
|
|
1159
|
+
eventQueue.push({ type: "error", message: line.trim(), recoverable: true });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
const stdoutDone = consumeStream(subprocess.stdout ?? void 0, "stdout");
|
|
1164
|
+
const stderrDone = consumeStream(subprocess.stderr ?? void 0, "stderr");
|
|
1165
|
+
const resultPromise = (async () => {
|
|
1166
|
+
try {
|
|
1167
|
+
const settled = await subprocess;
|
|
1168
|
+
await Promise.all([stdoutDone, stderrDone]);
|
|
1169
|
+
const timedOut = hasBooleanFlag3(settled, "timedOut");
|
|
1170
|
+
if (timedOut) {
|
|
1171
|
+
const timeoutError = executionError(
|
|
1172
|
+
"AGENT_TIMEOUT",
|
|
1173
|
+
`OpenCode execution timed out for ${params.executionId}`,
|
|
1174
|
+
{ context: { executionId: params.executionId, timeoutMs: params.timeoutMs } }
|
|
1175
|
+
);
|
|
1176
|
+
eventQueue.push({ type: "error", message: timeoutError.message, recoverable: true });
|
|
1177
|
+
throw timeoutError;
|
|
1178
|
+
}
|
|
1179
|
+
const canceled = hasBooleanFlag3(settled, "isCanceled");
|
|
1180
|
+
if (canceled) {
|
|
1181
|
+
return {
|
|
1182
|
+
success: false,
|
|
1183
|
+
exitCode: normalizeExitCode3(settled.exitCode),
|
|
1184
|
+
totalTokensUsed: computeTotalTokens3(tokenState),
|
|
1185
|
+
filesChanged: [...filesChanged],
|
|
1186
|
+
duration: Date.now() - startedAt,
|
|
1187
|
+
error: "OpenCode execution was cancelled."
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
const exitCode = normalizeExitCode3(settled.exitCode);
|
|
1191
|
+
const success = exitCode === 0;
|
|
1192
|
+
return {
|
|
1193
|
+
success,
|
|
1194
|
+
exitCode,
|
|
1195
|
+
totalTokensUsed: computeTotalTokens3(tokenState),
|
|
1196
|
+
filesChanged: [...filesChanged],
|
|
1197
|
+
duration: Date.now() - startedAt,
|
|
1198
|
+
error: success ? void 0 : buildFailureMessage3(settled.stdout, settled.stderr)
|
|
1199
|
+
};
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
const normalized = normalizeUnknownError3(error, params.executionId);
|
|
1202
|
+
eventQueue.push({
|
|
1203
|
+
type: "error",
|
|
1204
|
+
message: normalized.message,
|
|
1205
|
+
recoverable: normalized.severity !== "fatal"
|
|
1206
|
+
});
|
|
1207
|
+
eventQueue.fail(normalized);
|
|
1208
|
+
throw normalized;
|
|
1209
|
+
} finally {
|
|
1210
|
+
this.runningExecutions.delete(params.executionId);
|
|
1211
|
+
eventQueue.close();
|
|
1212
|
+
}
|
|
1213
|
+
})();
|
|
1214
|
+
return {
|
|
1215
|
+
executionId: params.executionId,
|
|
1216
|
+
providerId: this.id,
|
|
1217
|
+
events: eventQueue,
|
|
1218
|
+
result: resultPromise,
|
|
1219
|
+
pid: subprocess.pid
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
async estimateTokens(params) {
|
|
1223
|
+
const baseTokens = params.targetFiles.length * 2e3;
|
|
1224
|
+
const promptTokens = estimateTokenCount3(params.prompt);
|
|
1225
|
+
const contextTokens = await estimateContextTokens2(params.targetFiles);
|
|
1226
|
+
const expectedOutputTokens = baseTokens;
|
|
1227
|
+
const totalEstimatedTokens = contextTokens + promptTokens + expectedOutputTokens;
|
|
1228
|
+
return {
|
|
1229
|
+
taskId: params.taskId,
|
|
1230
|
+
providerId: this.id,
|
|
1231
|
+
contextTokens,
|
|
1232
|
+
promptTokens,
|
|
1233
|
+
expectedOutputTokens,
|
|
1234
|
+
totalEstimatedTokens,
|
|
1235
|
+
confidence: 0.5,
|
|
1236
|
+
feasible: totalEstimatedTokens < 2e5
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
async abort(executionId) {
|
|
1240
|
+
const running = this.runningExecutions.get(executionId);
|
|
1241
|
+
if (!running) return;
|
|
1242
|
+
running.kill("SIGTERM");
|
|
1243
|
+
const forceKillTimer = setTimeout(() => {
|
|
1244
|
+
running.kill("SIGKILL");
|
|
1245
|
+
}, 3e3);
|
|
1246
|
+
forceKillTimer.unref();
|
|
1247
|
+
try {
|
|
1248
|
+
await running;
|
|
1249
|
+
} catch {
|
|
1250
|
+
} finally {
|
|
1251
|
+
clearTimeout(forceKillTimer);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
// src/execution/agents/registry.ts
|
|
1257
|
+
var AdapterRegistry = class {
|
|
1258
|
+
factories = /* @__PURE__ */ new Map();
|
|
1259
|
+
/** Well-known aliases (e.g. legacy IDs) that map to canonical provider IDs. */
|
|
1260
|
+
aliases = /* @__PURE__ */ new Map([
|
|
1261
|
+
["codex-cli", "codex"]
|
|
1262
|
+
]);
|
|
1263
|
+
/** Register a new adapter factory. Replaces any previous factory for the same ID. */
|
|
1264
|
+
register(id, factory) {
|
|
1265
|
+
this.factories.set(id, factory);
|
|
1266
|
+
}
|
|
1267
|
+
/** Add an alias that maps to an existing canonical ID. */
|
|
1268
|
+
alias(alias, canonicalId) {
|
|
1269
|
+
this.aliases.set(alias, canonicalId);
|
|
1270
|
+
}
|
|
1271
|
+
/** Resolve an ID (including aliases) and return the factory, or `undefined`. */
|
|
1272
|
+
get(rawId) {
|
|
1273
|
+
const canonicalId = this.aliases.get(rawId) ?? rawId;
|
|
1274
|
+
return this.factories.get(canonicalId);
|
|
1275
|
+
}
|
|
1276
|
+
/** Canonical ID after alias resolution. */
|
|
1277
|
+
resolveId(rawId) {
|
|
1278
|
+
return this.aliases.get(rawId) ?? rawId;
|
|
1279
|
+
}
|
|
1280
|
+
/** All registered canonical provider IDs. */
|
|
1281
|
+
registeredIds() {
|
|
1282
|
+
return [...this.factories.keys()];
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
var adapterRegistry = new AdapterRegistry();
|
|
1286
|
+
adapterRegistry.register("claude-code", () => new ClaudeCodeAdapter());
|
|
1287
|
+
adapterRegistry.register("codex", () => new CodexAdapter());
|
|
1288
|
+
adapterRegistry.register("opencode", () => new OpenCodeAdapter());
|
|
1289
|
+
|
|
1290
|
+
// src/execution/sandbox.ts
|
|
1291
|
+
import { mkdir } from "fs/promises";
|
|
1292
|
+
import { join, resolve } from "path";
|
|
1293
|
+
import { simpleGit } from "simple-git";
|
|
1294
|
+
var worktreeLock = Promise.resolve();
|
|
1295
|
+
function withWorktreeLock(fn) {
|
|
1296
|
+
const next = worktreeLock.catch(() => {
|
|
1297
|
+
}).then(fn);
|
|
1298
|
+
worktreeLock = next.then(
|
|
1299
|
+
() => {
|
|
1300
|
+
},
|
|
1301
|
+
() => {
|
|
1302
|
+
}
|
|
1303
|
+
);
|
|
1304
|
+
return next;
|
|
1305
|
+
}
|
|
1306
|
+
function getWorktreePath(repoPath, branchName) {
|
|
1307
|
+
return resolve(join(repoPath, "..", ".oac-worktrees", branchName));
|
|
1308
|
+
}
|
|
1309
|
+
var SAFE_BRANCH_RE = /^[a-zA-Z0-9/_.-]+$/;
|
|
1310
|
+
async function createSandbox(repoPath, branchName, baseBranch) {
|
|
1311
|
+
if (!SAFE_BRANCH_RE.test(branchName)) {
|
|
1312
|
+
throw executionError("AGENT_EXECUTION_FAILED", `Invalid branch name: ${branchName}`);
|
|
1313
|
+
}
|
|
1314
|
+
if (!SAFE_BRANCH_RE.test(baseBranch)) {
|
|
1315
|
+
throw executionError("AGENT_EXECUTION_FAILED", `Invalid base branch name: ${baseBranch}`);
|
|
1316
|
+
}
|
|
1317
|
+
const worktreePath = getWorktreePath(repoPath, branchName);
|
|
1318
|
+
const worktreeRoot = resolve(join(repoPath, "..", ".oac-worktrees"));
|
|
1319
|
+
const git = simpleGit(repoPath);
|
|
1320
|
+
await withWorktreeLock(async () => {
|
|
1321
|
+
await mkdir(worktreeRoot, { recursive: true });
|
|
1322
|
+
await git.raw(["worktree", "add", worktreePath, "-b", branchName, `origin/${baseBranch}`]);
|
|
1323
|
+
});
|
|
1324
|
+
let cleanedUp = false;
|
|
1325
|
+
return {
|
|
1326
|
+
path: worktreePath,
|
|
1327
|
+
branchName,
|
|
1328
|
+
cleanup: async () => {
|
|
1329
|
+
if (cleanedUp) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
cleanedUp = true;
|
|
1333
|
+
await withWorktreeLock(async () => {
|
|
1334
|
+
try {
|
|
1335
|
+
await git.raw(["worktree", "remove", worktreePath, "--force"]);
|
|
1336
|
+
} finally {
|
|
1337
|
+
try {
|
|
1338
|
+
await git.raw(["worktree", "prune"]);
|
|
1339
|
+
} catch {
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/execution/worker.ts
|
|
1348
|
+
import { randomUUID } from "crypto";
|
|
1349
|
+
|
|
1350
|
+
// src/execution/normalize-error.ts
|
|
1351
|
+
function toErrorMessage(error) {
|
|
1352
|
+
if (error instanceof Error) {
|
|
1353
|
+
return error.message;
|
|
1354
|
+
}
|
|
1355
|
+
return String(error);
|
|
1356
|
+
}
|
|
1357
|
+
function normalizeExecutionError(error, context) {
|
|
1358
|
+
if (error instanceof OacError) {
|
|
1359
|
+
return error;
|
|
1360
|
+
}
|
|
1361
|
+
const message = toErrorMessage(error);
|
|
1362
|
+
const { jobId, taskId, attempt, executionId } = context;
|
|
1363
|
+
const ctx = { taskId };
|
|
1364
|
+
if (jobId) ctx.jobId = jobId;
|
|
1365
|
+
if (executionId) ctx.executionId = executionId;
|
|
1366
|
+
if (attempt !== void 0) ctx.attempt = attempt;
|
|
1367
|
+
ctx.message = message;
|
|
1368
|
+
if (/timed out|timeout/i.test(message)) {
|
|
1369
|
+
return executionError("AGENT_TIMEOUT", `Task ${taskId} timed out during execution.`, {
|
|
1370
|
+
context: ctx,
|
|
1371
|
+
cause: error
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
if (/out of memory|ENOMEM|heap/i.test(message)) {
|
|
1375
|
+
return executionError("AGENT_OOM", `Task ${taskId} ran out of memory.`, {
|
|
1376
|
+
context: ctx,
|
|
1377
|
+
cause: error
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
if (/network|ECONN|ENOTFOUND|EAI_AGAIN/i.test(message)) {
|
|
1381
|
+
return new OacError(
|
|
1382
|
+
`Task ${taskId} failed due to a network error.`,
|
|
1383
|
+
"NETWORK_ERROR",
|
|
1384
|
+
"recoverable",
|
|
1385
|
+
ctx,
|
|
1386
|
+
error
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
if (/index\.lock|cannot lock ref|Unable to create '.+?\.git\/index\.lock'/i.test(message)) {
|
|
1390
|
+
return new OacError(
|
|
1391
|
+
`Task ${taskId} failed due to a git lock conflict.`,
|
|
1392
|
+
"GIT_LOCK_FAILED",
|
|
1393
|
+
"recoverable",
|
|
1394
|
+
ctx,
|
|
1395
|
+
error
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
if (isRecord(error) && error.name === "AbortError") {
|
|
1399
|
+
return executionError("AGENT_EXECUTION_FAILED", `Task ${taskId} was aborted.`, {
|
|
1400
|
+
context: ctx,
|
|
1401
|
+
cause: error
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
return executionError("AGENT_EXECUTION_FAILED", `Task ${taskId} failed during execution.`, {
|
|
1405
|
+
context: ctx,
|
|
1406
|
+
cause: error
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// src/execution/worker.ts
|
|
1411
|
+
var DEFAULT_TOKEN_BUDGET = 5e4;
|
|
1412
|
+
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
1413
|
+
function readPositiveNumber(value) {
|
|
1414
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
1415
|
+
return void 0;
|
|
1416
|
+
}
|
|
1417
|
+
return Math.floor(value);
|
|
1418
|
+
}
|
|
1419
|
+
function readMetadataNumber(task, key) {
|
|
1420
|
+
return readPositiveNumber(task.metadata[key]);
|
|
1421
|
+
}
|
|
1422
|
+
function buildTaskPrompt(task) {
|
|
1423
|
+
const fileList = task.targetFiles.length > 0 ? task.targetFiles.join("\n") : "(none provided)";
|
|
1424
|
+
const lines = [
|
|
1425
|
+
"You are implementing a scoped repository contribution task.",
|
|
1426
|
+
`Task ID: ${task.id}`,
|
|
1427
|
+
`Title: ${task.title}`,
|
|
1428
|
+
`Source: ${task.source}`,
|
|
1429
|
+
`Priority: ${task.priority}`,
|
|
1430
|
+
`Complexity: ${task.complexity}`,
|
|
1431
|
+
`Execution mode: ${task.executionMode}`
|
|
1432
|
+
];
|
|
1433
|
+
if (task.linkedIssue) {
|
|
1434
|
+
lines.push(
|
|
1435
|
+
"",
|
|
1436
|
+
`GitHub Issue #${task.linkedIssue.number}: ${task.linkedIssue.url}`,
|
|
1437
|
+
task.linkedIssue.labels.length > 0 ? `Labels: ${task.linkedIssue.labels.join(", ")}` : "",
|
|
1438
|
+
"Resolve this issue completely. Read the issue description carefully and implement the fix."
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
lines.push(
|
|
1442
|
+
"",
|
|
1443
|
+
"Description:",
|
|
1444
|
+
task.description,
|
|
1445
|
+
"",
|
|
1446
|
+
"Target files:",
|
|
1447
|
+
fileList,
|
|
1448
|
+
"",
|
|
1449
|
+
"Apply minimal, safe changes and ensure the repository remains buildable."
|
|
1450
|
+
);
|
|
1451
|
+
return lines.filter((l) => l !== void 0).join("\n");
|
|
1452
|
+
}
|
|
1453
|
+
function stageFromEvent(event) {
|
|
1454
|
+
switch (event.type) {
|
|
1455
|
+
case "output":
|
|
1456
|
+
return event.stream;
|
|
1457
|
+
case "tokens":
|
|
1458
|
+
return "tokens";
|
|
1459
|
+
case "file_edit":
|
|
1460
|
+
return `file:${event.action}`;
|
|
1461
|
+
case "tool_use":
|
|
1462
|
+
return `tool:${event.tool}`;
|
|
1463
|
+
case "error":
|
|
1464
|
+
return event.recoverable ? "agent-warning" : "agent-error";
|
|
1465
|
+
default:
|
|
1466
|
+
return "running";
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
function mergeExecutionResult(result, observedTokens, observedFiles, startedAt) {
|
|
1470
|
+
for (const changedFile of result.filesChanged) {
|
|
1471
|
+
observedFiles.add(changedFile);
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
success: result.success,
|
|
1475
|
+
exitCode: result.exitCode,
|
|
1476
|
+
totalTokensUsed: Math.max(result.totalTokensUsed, observedTokens),
|
|
1477
|
+
filesChanged: [...observedFiles],
|
|
1478
|
+
duration: result.duration > 0 ? result.duration : Date.now() - startedAt,
|
|
1479
|
+
error: result.error
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
async function executeTask(agent, task, sandbox, eventBus, options = {}) {
|
|
1483
|
+
const executionId = options.executionId ?? randomUUID();
|
|
1484
|
+
const tokenBudget = options.tokenBudget ?? readMetadataNumber(task, "tokenBudget") ?? DEFAULT_TOKEN_BUDGET;
|
|
1485
|
+
const timeoutMs = options.timeoutMs ?? readMetadataNumber(task, "timeoutMs") ?? DEFAULT_TIMEOUT_MS;
|
|
1486
|
+
const allowCommits = options.allowCommits ?? true;
|
|
1487
|
+
const startedAt = Date.now();
|
|
1488
|
+
let observedTokens = 0;
|
|
1489
|
+
const observedFiles = /* @__PURE__ */ new Set();
|
|
1490
|
+
const execution = agent.execute({
|
|
1491
|
+
executionId,
|
|
1492
|
+
workingDirectory: sandbox.path,
|
|
1493
|
+
prompt: buildTaskPrompt(task),
|
|
1494
|
+
targetFiles: task.targetFiles,
|
|
1495
|
+
tokenBudget,
|
|
1496
|
+
allowCommits,
|
|
1497
|
+
timeoutMs
|
|
1498
|
+
});
|
|
1499
|
+
const streamPromise = (async () => {
|
|
1500
|
+
for await (const event of execution.events) {
|
|
1501
|
+
if (event.type === "tokens") {
|
|
1502
|
+
observedTokens = Math.max(observedTokens, event.cumulativeTokens);
|
|
1503
|
+
}
|
|
1504
|
+
if (event.type === "file_edit") {
|
|
1505
|
+
observedFiles.add(event.path);
|
|
1506
|
+
}
|
|
1507
|
+
eventBus.emit("execution:progress", {
|
|
1508
|
+
jobId: executionId,
|
|
1509
|
+
tokensUsed: observedTokens,
|
|
1510
|
+
stage: stageFromEvent(event)
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
})();
|
|
1514
|
+
try {
|
|
1515
|
+
const result = await execution.result;
|
|
1516
|
+
await streamPromise;
|
|
1517
|
+
return mergeExecutionResult(result, observedTokens, observedFiles, startedAt);
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
try {
|
|
1520
|
+
await streamPromise;
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
throw normalizeExecutionError(error, { taskId: task.id, executionId });
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function buildEpicPrompt(epic) {
|
|
1527
|
+
const lines = [
|
|
1528
|
+
"You are implementing a coherent set of changes as a single epic.",
|
|
1529
|
+
`Epic: ${epic.title}`,
|
|
1530
|
+
`Scope: ${epic.scope} module`,
|
|
1531
|
+
"",
|
|
1532
|
+
"Description:",
|
|
1533
|
+
epic.description,
|
|
1534
|
+
"",
|
|
1535
|
+
`Subtasks (${epic.subtasks.length}):`
|
|
1536
|
+
];
|
|
1537
|
+
for (let i = 0; i < epic.subtasks.length; i++) {
|
|
1538
|
+
const task = epic.subtasks[i];
|
|
1539
|
+
const files = task.targetFiles.length > 0 ? ` [${task.targetFiles.join(", ")}]` : "";
|
|
1540
|
+
lines.push(` ${i + 1}. ${task.title}${files}`);
|
|
1541
|
+
if (task.description) {
|
|
1542
|
+
lines.push(` ${task.description}`);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (epic.contextFiles.length > 0) {
|
|
1546
|
+
lines.push(
|
|
1547
|
+
"",
|
|
1548
|
+
"Context files to read for understanding:",
|
|
1549
|
+
...epic.contextFiles.map((f) => ` - ${f}`)
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
lines.push(
|
|
1553
|
+
"",
|
|
1554
|
+
"Instructions:",
|
|
1555
|
+
"- Apply all changes in a single coherent commit.",
|
|
1556
|
+
"- Ensure the repository remains buildable after changes.",
|
|
1557
|
+
"- Address all subtasks listed above."
|
|
1558
|
+
);
|
|
1559
|
+
return lines.join("\n");
|
|
1560
|
+
}
|
|
1561
|
+
function epicAsTask(epic) {
|
|
1562
|
+
const allTargetFiles = [...new Set(epic.subtasks.flatMap((t) => t.targetFiles))];
|
|
1563
|
+
return {
|
|
1564
|
+
id: epic.id,
|
|
1565
|
+
source: epic.subtasks[0]?.source ?? "custom",
|
|
1566
|
+
title: epic.title,
|
|
1567
|
+
description: buildEpicPrompt(epic),
|
|
1568
|
+
targetFiles: allTargetFiles,
|
|
1569
|
+
priority: epic.priority,
|
|
1570
|
+
complexity: epic.subtasks.length >= 7 ? "complex" : epic.subtasks.length >= 4 ? "moderate" : "simple",
|
|
1571
|
+
executionMode: "new-pr",
|
|
1572
|
+
metadata: { epicId: epic.id, subtaskCount: epic.subtasks.length },
|
|
1573
|
+
discoveredAt: epic.createdAt,
|
|
1574
|
+
parentEpicId: void 0
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// src/execution/engine.ts
|
|
1579
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1580
|
+
import { setTimeout as delay } from "timers/promises";
|
|
1581
|
+
import PQueue from "p-queue";
|
|
1582
|
+
var DEFAULT_CONCURRENCY = 2;
|
|
1583
|
+
var DEFAULT_MAX_ATTEMPTS = 2;
|
|
1584
|
+
var DEFAULT_TIMEOUT_MS2 = 3e5;
|
|
1585
|
+
var DEFAULT_TOKEN_BUDGET2 = 5e4;
|
|
1586
|
+
function sanitizeBranchSegment(value) {
|
|
1587
|
+
const sanitized = value.toLowerCase().replace(/[^a-z0-9/_-]+/g, "-").replace(/-+/g, "-").replace(/^[-/]+|[-/]+$/g, "");
|
|
1588
|
+
return sanitized || "task";
|
|
1589
|
+
}
|
|
1590
|
+
function isTransientError(error) {
|
|
1591
|
+
return error.code === "AGENT_TIMEOUT" || error.code === "AGENT_OOM" || error.code === "AGENT_RATE_LIMITED" || error.code === "NETWORK_ERROR" || error.code === "GIT_LOCK_FAILED";
|
|
1592
|
+
}
|
|
1593
|
+
var ExecutionEngine = class {
|
|
1594
|
+
constructor(agents, eventBus, config = {}) {
|
|
1595
|
+
this.agents = agents;
|
|
1596
|
+
this.eventBus = eventBus;
|
|
1597
|
+
if (agents.length === 0) {
|
|
1598
|
+
throw executionError(
|
|
1599
|
+
"AGENT_NOT_AVAILABLE",
|
|
1600
|
+
"ExecutionEngine requires at least one agent provider"
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
this.concurrency = Math.max(1, config.concurrency ?? DEFAULT_CONCURRENCY);
|
|
1604
|
+
this.maxAttempts = Math.max(1, config.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
|
|
1605
|
+
this.repoPath = config.repoPath ?? process.cwd();
|
|
1606
|
+
this.baseBranch = config.baseBranch ?? "main";
|
|
1607
|
+
this.branchPrefix = config.branchPrefix ?? "oac";
|
|
1608
|
+
this.taskTimeoutMs = Math.max(1, config.taskTimeoutMs ?? DEFAULT_TIMEOUT_MS2);
|
|
1609
|
+
this.defaultTokenBudget = Math.max(1, config.defaultTokenBudget ?? DEFAULT_TOKEN_BUDGET2);
|
|
1610
|
+
this.queue = new PQueue({
|
|
1611
|
+
concurrency: this.concurrency,
|
|
1612
|
+
autoStart: false
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
queue;
|
|
1616
|
+
jobs = /* @__PURE__ */ new Map();
|
|
1617
|
+
activeJobs = /* @__PURE__ */ new Map();
|
|
1618
|
+
concurrency;
|
|
1619
|
+
maxAttempts;
|
|
1620
|
+
repoPath;
|
|
1621
|
+
baseBranch;
|
|
1622
|
+
branchPrefix;
|
|
1623
|
+
taskTimeoutMs;
|
|
1624
|
+
defaultTokenBudget;
|
|
1625
|
+
aborted = false;
|
|
1626
|
+
nextAgentIndex = 0;
|
|
1627
|
+
enqueue(plan) {
|
|
1628
|
+
const enqueuedJobs = [];
|
|
1629
|
+
for (const { task, estimate } of plan.selectedTasks) {
|
|
1630
|
+
const job = {
|
|
1631
|
+
id: randomUUID2(),
|
|
1632
|
+
task,
|
|
1633
|
+
estimate,
|
|
1634
|
+
status: "queued",
|
|
1635
|
+
attempts: 0,
|
|
1636
|
+
maxAttempts: this.maxAttempts,
|
|
1637
|
+
createdAt: Date.now()
|
|
1638
|
+
};
|
|
1639
|
+
this.jobs.set(job.id, job);
|
|
1640
|
+
enqueuedJobs.push(job);
|
|
1641
|
+
this.schedule(job);
|
|
1642
|
+
}
|
|
1643
|
+
return enqueuedJobs;
|
|
1644
|
+
}
|
|
1645
|
+
async run() {
|
|
1646
|
+
this.aborted = false;
|
|
1647
|
+
this.queue.start();
|
|
1648
|
+
await this.queue.onIdle();
|
|
1649
|
+
return this.buildRunResult();
|
|
1650
|
+
}
|
|
1651
|
+
async abort() {
|
|
1652
|
+
this.aborted = true;
|
|
1653
|
+
this.queue.pause();
|
|
1654
|
+
this.queue.clear();
|
|
1655
|
+
const abortError = executionError("AGENT_EXECUTION_FAILED", "Execution aborted by user.");
|
|
1656
|
+
for (const job of this.jobs.values()) {
|
|
1657
|
+
if (job.status === "queued" || job.status === "retrying") {
|
|
1658
|
+
job.status = "aborted";
|
|
1659
|
+
job.completedAt = Date.now();
|
|
1660
|
+
job.error = abortError;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
await Promise.all(
|
|
1664
|
+
[...this.activeJobs.values()].map(async ({ job, agent }) => {
|
|
1665
|
+
job.status = "aborted";
|
|
1666
|
+
job.completedAt = Date.now();
|
|
1667
|
+
job.error = abortError;
|
|
1668
|
+
this.eventBus.emit("execution:failed", {
|
|
1669
|
+
jobId: job.id,
|
|
1670
|
+
error: abortError
|
|
1671
|
+
});
|
|
1672
|
+
try {
|
|
1673
|
+
await agent.abort(job.id);
|
|
1674
|
+
} catch {
|
|
1675
|
+
}
|
|
1676
|
+
})
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
schedule(job, delayMs = 0) {
|
|
1680
|
+
void this.queue.add(
|
|
1681
|
+
async () => {
|
|
1682
|
+
if (delayMs > 0) {
|
|
1683
|
+
await delay(delayMs);
|
|
1684
|
+
}
|
|
1685
|
+
await this.runJob(job);
|
|
1686
|
+
},
|
|
1687
|
+
{ priority: job.task.priority }
|
|
1688
|
+
).catch((error) => {
|
|
1689
|
+
const normalized = this.normalizeError(error, job);
|
|
1690
|
+
job.status = "failed";
|
|
1691
|
+
job.completedAt = Date.now();
|
|
1692
|
+
job.error = normalized;
|
|
1693
|
+
this.eventBus.emit("execution:failed", {
|
|
1694
|
+
jobId: job.id,
|
|
1695
|
+
error: normalized
|
|
1696
|
+
});
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
async runJob(job) {
|
|
1700
|
+
if (this.aborted || job.status === "aborted") {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
job.attempts += 1;
|
|
1704
|
+
job.status = "running";
|
|
1705
|
+
job.startedAt ??= Date.now();
|
|
1706
|
+
const agent = this.selectAgent();
|
|
1707
|
+
job.workerId = agent.id;
|
|
1708
|
+
this.activeJobs.set(job.id, { job, agent });
|
|
1709
|
+
this.eventBus.emit("execution:started", {
|
|
1710
|
+
jobId: job.id,
|
|
1711
|
+
task: job.task,
|
|
1712
|
+
agent: agent.id
|
|
1713
|
+
});
|
|
1714
|
+
let sandboxCleanup;
|
|
1715
|
+
try {
|
|
1716
|
+
const branchName = this.createBranchName(job);
|
|
1717
|
+
const sandbox = await createSandbox(this.repoPath, branchName, this.baseBranch);
|
|
1718
|
+
sandboxCleanup = sandbox.cleanup;
|
|
1719
|
+
const result = await executeTask(agent, job.task, sandbox, this.eventBus, {
|
|
1720
|
+
executionId: job.id,
|
|
1721
|
+
tokenBudget: job.estimate.totalEstimatedTokens > 0 ? job.estimate.totalEstimatedTokens : this.defaultTokenBudget,
|
|
1722
|
+
timeoutMs: this.taskTimeoutMs,
|
|
1723
|
+
allowCommits: true
|
|
1724
|
+
});
|
|
1725
|
+
job.result = result;
|
|
1726
|
+
job.completedAt = Date.now();
|
|
1727
|
+
if (result.success) {
|
|
1728
|
+
job.status = "completed";
|
|
1729
|
+
this.eventBus.emit("execution:completed", {
|
|
1730
|
+
jobId: job.id,
|
|
1731
|
+
result
|
|
1732
|
+
});
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const failure = executionError(
|
|
1736
|
+
"AGENT_EXECUTION_FAILED",
|
|
1737
|
+
result.error ?? `Task ${job.task.id} exited with code ${result.exitCode}.`,
|
|
1738
|
+
{
|
|
1739
|
+
context: {
|
|
1740
|
+
taskId: job.task.id,
|
|
1741
|
+
jobId: job.id,
|
|
1742
|
+
exitCode: result.exitCode,
|
|
1743
|
+
attempt: job.attempts
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
);
|
|
1747
|
+
await this.handleFailure(job, failure);
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
const normalized = this.normalizeError(error, job);
|
|
1750
|
+
await this.handleFailure(job, normalized);
|
|
1751
|
+
} finally {
|
|
1752
|
+
this.activeJobs.delete(job.id);
|
|
1753
|
+
if (sandboxCleanup) {
|
|
1754
|
+
try {
|
|
1755
|
+
await sandboxCleanup();
|
|
1756
|
+
} catch (cleanupError) {
|
|
1757
|
+
const cleanupMessage = toErrorMessage(cleanupError);
|
|
1758
|
+
job.error ??= executionError(
|
|
1759
|
+
"AGENT_EXECUTION_FAILED",
|
|
1760
|
+
`Sandbox cleanup failed for job ${job.id}`,
|
|
1761
|
+
{
|
|
1762
|
+
context: {
|
|
1763
|
+
jobId: job.id,
|
|
1764
|
+
cleanupError: cleanupMessage
|
|
1765
|
+
},
|
|
1766
|
+
cause: cleanupError
|
|
1767
|
+
}
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
async handleFailure(job, error) {
|
|
1774
|
+
job.error = error;
|
|
1775
|
+
if (this.aborted || job.status === "aborted") {
|
|
1776
|
+
job.status = "aborted";
|
|
1777
|
+
job.completedAt = Date.now();
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (job.attempts < job.maxAttempts && isTransientError(error)) {
|
|
1781
|
+
job.status = "retrying";
|
|
1782
|
+
const retryDelay = error.code === "AGENT_RATE_LIMITED" ? Math.min(6e4, 1e4 * 2 ** (job.attempts - 1)) : Math.min(5e3, job.attempts * 1e3);
|
|
1783
|
+
this.schedule(job, retryDelay);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
job.status = "failed";
|
|
1787
|
+
job.completedAt = Date.now();
|
|
1788
|
+
this.eventBus.emit("execution:failed", {
|
|
1789
|
+
jobId: job.id,
|
|
1790
|
+
error
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
selectAgent() {
|
|
1794
|
+
const agent = this.agents[this.nextAgentIndex % this.agents.length];
|
|
1795
|
+
this.nextAgentIndex = (this.nextAgentIndex + 1) % this.agents.length;
|
|
1796
|
+
return agent;
|
|
1797
|
+
}
|
|
1798
|
+
createBranchName(job) {
|
|
1799
|
+
const dateSegment = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replaceAll("-", "");
|
|
1800
|
+
const taskSegment = sanitizeBranchSegment(job.task.id);
|
|
1801
|
+
return `${this.branchPrefix}/${dateSegment}/${taskSegment}-${job.id.slice(0, 8)}-a${job.attempts}`;
|
|
1802
|
+
}
|
|
1803
|
+
normalizeError(error, job) {
|
|
1804
|
+
return normalizeExecutionError(error, {
|
|
1805
|
+
jobId: job.id,
|
|
1806
|
+
taskId: job.task.id,
|
|
1807
|
+
attempt: job.attempts
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
buildRunResult() {
|
|
1811
|
+
const jobs = [...this.jobs.values()];
|
|
1812
|
+
return {
|
|
1813
|
+
jobs,
|
|
1814
|
+
completed: jobs.filter((job) => job.status === "completed"),
|
|
1815
|
+
failed: jobs.filter((job) => job.status === "failed"),
|
|
1816
|
+
aborted: jobs.filter((job) => job.status === "aborted")
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
export {
|
|
1822
|
+
ClaudeCodeAdapter,
|
|
1823
|
+
CodexAdapter,
|
|
1824
|
+
OpenCodeAdapter,
|
|
1825
|
+
adapterRegistry,
|
|
1826
|
+
createSandbox,
|
|
1827
|
+
executeTask,
|
|
1828
|
+
buildEpicPrompt,
|
|
1829
|
+
epicAsTask,
|
|
1830
|
+
isTransientError,
|
|
1831
|
+
ExecutionEngine
|
|
1832
|
+
};
|
|
1833
|
+
//# sourceMappingURL=chunk-QPVNC7S4.js.map
|