@ekairos/openai-reactor 1.22.34-beta.development.0 → 1.22.35
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/README.md +15 -62
- package/dist/codex.reactor.d.ts +63 -5
- package/dist/codex.reactor.d.ts.map +1 -1
- package/dist/codex.reactor.js +1564 -91
- package/dist/codex.reactor.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/responses.reactor.d.ts +89 -0
- package/dist/responses.reactor.d.ts.map +1 -0
- package/dist/responses.reactor.js +1002 -0
- package/dist/responses.reactor.js.map +1 -0
- package/dist/responses.websocket.d.ts +52 -0
- package/dist/responses.websocket.d.ts.map +1 -0
- package/dist/responses.websocket.js +548 -0
- package/dist/responses.websocket.js.map +1 -0
- package/dist/shared.d.ts +12 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +407 -22
- package/dist/shared.js.map +1 -1
- package/package.json +14 -5
package/dist/codex.reactor.js
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
|
-
import { OUTPUT_ITEM_TYPE, createContextStepStreamChunk, encodeContextStepStreamChunk, } from "@ekairos/events";
|
|
2
|
-
import {
|
|
1
|
+
import { OUTPUT_ITEM_TYPE, createContextStepStreamChunk, encodeContextStepStreamChunk, resolveContextPartChunkDescriptor, resolveContextPartChunkIdentity, actionsToActionSpecs, } from "@ekairos/events";
|
|
2
|
+
import { SANDBOX_EXECUTE_COMMAND_ACTION_NAME } from "@ekairos/sandbox/contract";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { asRecord, asString, buildCodexParts, defaultInstructionFromTrigger, readCodexDynamicActionDetails, } from "./shared.js";
|
|
5
|
+
function isCodexExecutableAction(value) {
|
|
6
|
+
return (typeof value === "object" &&
|
|
7
|
+
value !== null &&
|
|
8
|
+
"execute" in value &&
|
|
9
|
+
typeof value.execute === "function");
|
|
10
|
+
}
|
|
11
|
+
function isCodexSandboxRuntime(value) {
|
|
12
|
+
return (typeof value === "object" &&
|
|
13
|
+
value !== null &&
|
|
14
|
+
"use" in value &&
|
|
15
|
+
typeof value.use === "function");
|
|
16
|
+
}
|
|
17
|
+
function hasStreamCapableSandboxDb(value) {
|
|
18
|
+
const db = asRecord(value);
|
|
19
|
+
const streams = asRecord(db.streams);
|
|
20
|
+
const tx = asRecord(db.tx);
|
|
21
|
+
return (typeof db.transact === "function" &&
|
|
22
|
+
typeof streams.createWriteStream === "function" &&
|
|
23
|
+
typeof tx.sandbox_processes === "object" &&
|
|
24
|
+
tx.sandbox_processes !== null);
|
|
25
|
+
}
|
|
3
26
|
const PROVIDER_SCOPE_PREFIX = "context/";
|
|
4
27
|
const PROVIDER_STARTED = "context/started";
|
|
5
28
|
const PROVIDER_ARCHIVED = "context/archived";
|
|
@@ -14,6 +37,9 @@ function toJsonSafe(value) {
|
|
|
14
37
|
return undefined;
|
|
15
38
|
}
|
|
16
39
|
}
|
|
40
|
+
function cleanRecord(value) {
|
|
41
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
42
|
+
}
|
|
17
43
|
export function mapCodexChunkType(providerChunkType) {
|
|
18
44
|
const value = providerChunkType.toLowerCase();
|
|
19
45
|
if (value.includes("start_step"))
|
|
@@ -30,22 +56,27 @@ export function mapCodexChunkType(providerChunkType) {
|
|
|
30
56
|
return "chunk.reasoning_delta";
|
|
31
57
|
if (value.includes("reasoning_end"))
|
|
32
58
|
return "chunk.reasoning_end";
|
|
33
|
-
if (value.includes("
|
|
34
|
-
return "chunk.
|
|
35
|
-
|
|
59
|
+
if (value.includes("action_started") || value.includes("tool_started"))
|
|
60
|
+
return "chunk.action_started";
|
|
61
|
+
if (value.includes("action_completed") || value.includes("tool_completed"))
|
|
62
|
+
return "chunk.action_completed";
|
|
63
|
+
if (value.includes("action_failed") || value.includes("tool_failed"))
|
|
64
|
+
return "chunk.action_failed";
|
|
65
|
+
if (value.includes("action_input_start") || value.includes("tool_input_start"))
|
|
66
|
+
return "chunk.action_started";
|
|
36
67
|
if (value.includes("action_input_delta") || value.includes("tool_input_delta")) {
|
|
37
68
|
return "chunk.action_input_delta";
|
|
38
69
|
}
|
|
39
70
|
if (value.includes("action_input_available") ||
|
|
40
71
|
value.includes("tool_input_available") ||
|
|
41
72
|
value.includes("action_call")) {
|
|
42
|
-
return "chunk.
|
|
73
|
+
return "chunk.action_started";
|
|
43
74
|
}
|
|
44
75
|
if (value.includes("action_output_available") || value.includes("tool_output_available")) {
|
|
45
|
-
return "chunk.
|
|
76
|
+
return "chunk.action_completed";
|
|
46
77
|
}
|
|
47
78
|
if (value.includes("action_output_error") || value.includes("tool_output_error")) {
|
|
48
|
-
return "chunk.
|
|
79
|
+
return "chunk.action_failed";
|
|
49
80
|
}
|
|
50
81
|
if (value.includes("message_metadata"))
|
|
51
82
|
return "chunk.message_metadata";
|
|
@@ -89,6 +120,19 @@ function isActionItemType(itemType) {
|
|
|
89
120
|
function resolveActionRef(params, item) {
|
|
90
121
|
const fromParams = asString(params.itemId) ||
|
|
91
122
|
asString(params.toolCallId) ||
|
|
123
|
+
asString(params.callId) ||
|
|
124
|
+
asString(params.id);
|
|
125
|
+
if (fromParams)
|
|
126
|
+
return fromParams;
|
|
127
|
+
const fromItem = asString(item.id) || asString(item.toolCallId);
|
|
128
|
+
if (fromItem)
|
|
129
|
+
return fromItem;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
function resolveProviderPartId(params, item) {
|
|
133
|
+
const fromParams = asString(params.itemId) ||
|
|
134
|
+
asString(params.toolCallId) ||
|
|
135
|
+
asString(params.callId) ||
|
|
92
136
|
asString(params.id);
|
|
93
137
|
if (fromParams)
|
|
94
138
|
return fromParams;
|
|
@@ -97,6 +141,39 @@ function resolveActionRef(params, item) {
|
|
|
97
141
|
return fromItem;
|
|
98
142
|
return undefined;
|
|
99
143
|
}
|
|
144
|
+
function withCodexPartDescriptor(chunkType, providerPartId) {
|
|
145
|
+
return resolveContextPartChunkDescriptor({
|
|
146
|
+
chunkType,
|
|
147
|
+
providerPartId,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function completeCodexMappedChunkIdentity(mapped, stepId) {
|
|
151
|
+
const data = asRecord(mapped.data);
|
|
152
|
+
const providerPartId = asString(mapped.providerPartId) ||
|
|
153
|
+
(mapped.chunkType.startsWith("chunk.action_") ? asString(mapped.actionRef) : "") ||
|
|
154
|
+
asString(data.providerPartId) ||
|
|
155
|
+
asString(data.id) ||
|
|
156
|
+
asString(data.itemId) ||
|
|
157
|
+
asString(data.toolCallId) ||
|
|
158
|
+
asString(mapped.actionRef);
|
|
159
|
+
const identity = resolveContextPartChunkIdentity({
|
|
160
|
+
stepId,
|
|
161
|
+
provider: "codex",
|
|
162
|
+
providerPartId,
|
|
163
|
+
chunkType: mapped.chunkType,
|
|
164
|
+
partType: mapped.partType,
|
|
165
|
+
partSlot: mapped.partSlot,
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
partId: identity?.partId,
|
|
169
|
+
providerPartId: identity?.providerPartId,
|
|
170
|
+
partType: identity?.partType,
|
|
171
|
+
partSlot: identity?.partSlot,
|
|
172
|
+
actionRef: mapped.chunkType.startsWith("chunk.action_")
|
|
173
|
+
? asString(mapped.actionRef) || identity?.providerPartId
|
|
174
|
+
: undefined,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
100
177
|
export function mapCodexAppServerNotification(providerChunk) {
|
|
101
178
|
const chunk = asRecord(providerChunk);
|
|
102
179
|
const method = asString(chunk.method).trim();
|
|
@@ -120,18 +197,73 @@ export function mapCodexAppServerNotification(providerChunk) {
|
|
|
120
197
|
const itemType = normalizeLower(item.type);
|
|
121
198
|
const itemStatus = normalizeLower(item.status);
|
|
122
199
|
const actionRef = resolveActionRef(params, item);
|
|
200
|
+
const providerPartId = resolveProviderPartId(params, item);
|
|
123
201
|
const hasItemError = Boolean(item.error);
|
|
124
|
-
const
|
|
202
|
+
const actionDetails = readCodexDynamicActionDetails(params);
|
|
203
|
+
const isCommandExecutionItem = itemType === "commandexecution";
|
|
204
|
+
const isCommandExecutionOutputDelta = method === "item/commandExecution/outputDelta";
|
|
205
|
+
const commandExecutionInput = isCommandExecutionItem
|
|
206
|
+
? cleanRecord({
|
|
207
|
+
command: asString(item.command),
|
|
208
|
+
args: [],
|
|
209
|
+
cwd: asString(item.cwd) || undefined,
|
|
210
|
+
kind: "command",
|
|
211
|
+
mode: "foreground",
|
|
212
|
+
metadata: cleanRecord({
|
|
213
|
+
source: "codex.commandExecution",
|
|
214
|
+
commandActions: Array.isArray(item.commandActions) ? item.commandActions : [],
|
|
215
|
+
}),
|
|
216
|
+
})
|
|
217
|
+
: undefined;
|
|
218
|
+
const commandExecutionOutput = isCommandExecutionItem
|
|
219
|
+
? cleanRecord({
|
|
220
|
+
success: itemStatus === "failed"
|
|
221
|
+
? false
|
|
222
|
+
: typeof item.exitCode === "number"
|
|
223
|
+
? item.exitCode === 0
|
|
224
|
+
: undefined,
|
|
225
|
+
exitCode: typeof item.exitCode === "number" ? item.exitCode : undefined,
|
|
226
|
+
output: asString(item.aggregatedOutput) || undefined,
|
|
227
|
+
error: asString(item.error || item.message) || undefined,
|
|
228
|
+
command: asString(item.command) || undefined,
|
|
229
|
+
durationMs: typeof item.durationMs === "number" ? item.durationMs : undefined,
|
|
230
|
+
status: itemStatus === "failed" || (typeof item.exitCode === "number" && item.exitCode !== 0)
|
|
231
|
+
? "failed"
|
|
232
|
+
: itemStatus === "completed"
|
|
233
|
+
? "exited"
|
|
234
|
+
: itemStatus || undefined,
|
|
235
|
+
})
|
|
236
|
+
: undefined;
|
|
237
|
+
const mappedData = toJsonSafe(cleanRecord({
|
|
125
238
|
method,
|
|
126
239
|
params,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
240
|
+
actionCallId: actionDetails.actionCallId,
|
|
241
|
+
actionName: actionDetails.actionName ||
|
|
242
|
+
(isCommandExecutionItem || isCommandExecutionOutputDelta
|
|
243
|
+
? SANDBOX_EXECUTE_COMMAND_ACTION_NAME
|
|
244
|
+
: undefined),
|
|
245
|
+
input: actionDetails.input ?? commandExecutionInput,
|
|
246
|
+
output: actionDetails.output ??
|
|
247
|
+
commandExecutionOutput ??
|
|
248
|
+
(isCommandExecutionOutputDelta
|
|
249
|
+
? cleanRecord({ output: asString(params.delta) || undefined })
|
|
250
|
+
: undefined),
|
|
251
|
+
success: actionDetails.success,
|
|
252
|
+
errorText: actionDetails.errorText,
|
|
253
|
+
}));
|
|
254
|
+
const map = (chunkType) => {
|
|
255
|
+
const descriptor = withCodexPartDescriptor(chunkType, providerPartId);
|
|
256
|
+
return {
|
|
257
|
+
chunkType,
|
|
258
|
+
providerChunkType: method,
|
|
259
|
+
providerPartId: descriptor?.providerPartId,
|
|
260
|
+
partType: descriptor?.partType,
|
|
261
|
+
partSlot: descriptor?.partSlot,
|
|
262
|
+
actionRef: chunkType.startsWith("chunk.action_") ? actionRef : undefined,
|
|
263
|
+
data: mappedData,
|
|
264
|
+
raw: toJsonSafe(providerChunk),
|
|
265
|
+
};
|
|
266
|
+
};
|
|
135
267
|
switch (method) {
|
|
136
268
|
case "turn/started":
|
|
137
269
|
return map("chunk.start");
|
|
@@ -168,7 +300,14 @@ export function mapCodexAppServerNotification(providerChunk) {
|
|
|
168
300
|
case "item/commandExecution/outputDelta":
|
|
169
301
|
case "item/fileChange/outputDelta":
|
|
170
302
|
case "item/mcpToolCall/progress":
|
|
171
|
-
return map("chunk.
|
|
303
|
+
return map("chunk.action_completed");
|
|
304
|
+
case "item/tool/call":
|
|
305
|
+
return map("chunk.action_started");
|
|
306
|
+
case "item/tool/result":
|
|
307
|
+
if (asRecord(params.result).success === false || asString(params.error)) {
|
|
308
|
+
return map("chunk.action_failed");
|
|
309
|
+
}
|
|
310
|
+
return map("chunk.action_completed");
|
|
172
311
|
case "item/started": {
|
|
173
312
|
if (itemType === "agentmessage")
|
|
174
313
|
return map("chunk.text_start");
|
|
@@ -177,7 +316,7 @@ export function mapCodexAppServerNotification(providerChunk) {
|
|
|
177
316
|
if (itemType === "usermessage")
|
|
178
317
|
return map("chunk.message_metadata");
|
|
179
318
|
if (isActionItemType(itemType))
|
|
180
|
-
return map("chunk.
|
|
319
|
+
return map("chunk.action_started");
|
|
181
320
|
return map("chunk.message_metadata");
|
|
182
321
|
}
|
|
183
322
|
case "item/completed": {
|
|
@@ -189,9 +328,9 @@ export function mapCodexAppServerNotification(providerChunk) {
|
|
|
189
328
|
return map("chunk.message_metadata");
|
|
190
329
|
if (isActionItemType(itemType)) {
|
|
191
330
|
if (hasItemError || itemStatus === "failed" || itemStatus === "declined") {
|
|
192
|
-
return map("chunk.
|
|
331
|
+
return map("chunk.action_failed");
|
|
193
332
|
}
|
|
194
|
-
return map("chunk.
|
|
333
|
+
return map("chunk.action_completed");
|
|
195
334
|
}
|
|
196
335
|
if (hasItemError || itemStatus === "failed" || itemStatus === "declined") {
|
|
197
336
|
return map("chunk.error");
|
|
@@ -218,12 +357,25 @@ export function defaultMapCodexChunk(providerChunk) {
|
|
|
218
357
|
const chunk = asRecord(providerChunk);
|
|
219
358
|
const providerChunkType = asString(chunk.type) || "unknown";
|
|
220
359
|
const actionRef = asString(chunk.actionRef) || asString(chunk.toolCallId) || asString(chunk.id) || undefined;
|
|
360
|
+
const providerPartId = asString(chunk.providerPartId) ||
|
|
361
|
+
asString(chunk.itemId) ||
|
|
362
|
+
asString(chunk.toolCallId) ||
|
|
363
|
+
asString(chunk.id) ||
|
|
364
|
+
asString(chunk.actionRef) ||
|
|
365
|
+
undefined;
|
|
366
|
+
const chunkType = mapCodexChunkType(providerChunkType);
|
|
367
|
+
const descriptor = withCodexPartDescriptor(chunkType, providerPartId);
|
|
221
368
|
return {
|
|
222
|
-
chunkType
|
|
369
|
+
chunkType,
|
|
223
370
|
providerChunkType,
|
|
371
|
+
providerPartId: descriptor?.providerPartId,
|
|
372
|
+
partType: descriptor?.partType,
|
|
373
|
+
partSlot: descriptor?.partSlot,
|
|
224
374
|
actionRef,
|
|
225
375
|
data: toJsonSafe({
|
|
226
376
|
id: chunk.id,
|
|
377
|
+
itemId: chunk.itemId,
|
|
378
|
+
providerPartId: descriptor?.providerPartId,
|
|
227
379
|
delta: chunk.delta,
|
|
228
380
|
text: chunk.text,
|
|
229
381
|
finishReason: chunk.finishReason,
|
|
@@ -278,6 +430,1246 @@ function extractUsageMetrics(usageSource) {
|
|
|
278
430
|
promptTokensUncached,
|
|
279
431
|
};
|
|
280
432
|
}
|
|
433
|
+
function asUnknownArray(value) {
|
|
434
|
+
return Array.isArray(value) ? value : [];
|
|
435
|
+
}
|
|
436
|
+
function asNumberRecord(value) {
|
|
437
|
+
const record = asRecord(value);
|
|
438
|
+
const out = {};
|
|
439
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
440
|
+
if (typeof entry === "number" && Number.isFinite(entry)) {
|
|
441
|
+
out[key] = entry;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
function isValidProviderContextId(value) {
|
|
447
|
+
const normalized = value.trim();
|
|
448
|
+
if (!normalized)
|
|
449
|
+
return false;
|
|
450
|
+
if (/^[0-9a-fA-F-]{36}$/.test(normalized))
|
|
451
|
+
return true;
|
|
452
|
+
if (/^urn:uuid:[0-9a-fA-F-]{36}$/.test(normalized))
|
|
453
|
+
return true;
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
function normalizeAppServerBaseUrl(raw) {
|
|
457
|
+
const trimmed = String(raw || "").trim().replace(/\/+$/, "");
|
|
458
|
+
if (trimmed.endsWith("/turn"))
|
|
459
|
+
return trimmed.slice(0, -"/turn".length);
|
|
460
|
+
if (trimmed.endsWith("/rpc"))
|
|
461
|
+
return trimmed.slice(0, -"/rpc".length);
|
|
462
|
+
if (trimmed.endsWith("/events"))
|
|
463
|
+
return trimmed.slice(0, -"/events".length);
|
|
464
|
+
return trimmed;
|
|
465
|
+
}
|
|
466
|
+
function parseSseDataBlock(block) {
|
|
467
|
+
const lines = block.split("\n").map((line) => line.trimEnd());
|
|
468
|
+
const dataLines = lines.filter((line) => line.startsWith("data:"));
|
|
469
|
+
if (!dataLines.length)
|
|
470
|
+
return null;
|
|
471
|
+
return dataLines.map((line) => line.replace(/^data:\s*/, "")).join("\n");
|
|
472
|
+
}
|
|
473
|
+
function shellSingleQuote(value) {
|
|
474
|
+
return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
|
|
475
|
+
}
|
|
476
|
+
function stripProviderControlChars(value) {
|
|
477
|
+
return String(value ?? "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, "");
|
|
478
|
+
}
|
|
479
|
+
function parseSandboxJsonl(stdout) {
|
|
480
|
+
const events = [];
|
|
481
|
+
let result = {};
|
|
482
|
+
for (const rawLine of stripProviderControlChars(stdout).split(/\r?\n/)) {
|
|
483
|
+
const line = rawLine.trim();
|
|
484
|
+
if (!line.startsWith("EKAIROS_CODEX_"))
|
|
485
|
+
continue;
|
|
486
|
+
const tab = line.indexOf("\t");
|
|
487
|
+
if (tab === -1)
|
|
488
|
+
continue;
|
|
489
|
+
const prefix = line.slice(0, tab);
|
|
490
|
+
const payload = asRecord(JSON.parse(line.slice(tab + 1)));
|
|
491
|
+
if (prefix === "EKAIROS_CODEX_EVENT")
|
|
492
|
+
events.push(payload);
|
|
493
|
+
if (prefix === "EKAIROS_CODEX_RESULT")
|
|
494
|
+
result = payload;
|
|
495
|
+
}
|
|
496
|
+
return { events, result };
|
|
497
|
+
}
|
|
498
|
+
function buildCodexDynamicTools(actionSpecs) {
|
|
499
|
+
const specs = actionSpecs && typeof actionSpecs === "object" ? actionSpecs : {};
|
|
500
|
+
return Object.entries(specs)
|
|
501
|
+
.map(([name, spec]) => {
|
|
502
|
+
const toolName = asString(name).trim();
|
|
503
|
+
if (!toolName)
|
|
504
|
+
return null;
|
|
505
|
+
return {
|
|
506
|
+
name: toolName,
|
|
507
|
+
description: asString(spec?.description).trim() || `Run ${toolName}.`,
|
|
508
|
+
inputSchema: spec && "inputSchema" in spec && spec.inputSchema !== undefined
|
|
509
|
+
? spec.inputSchema
|
|
510
|
+
: { type: "object", additionalProperties: true },
|
|
511
|
+
};
|
|
512
|
+
})
|
|
513
|
+
.filter(Boolean);
|
|
514
|
+
}
|
|
515
|
+
function toCodexActionSpecs(value) {
|
|
516
|
+
const specs = asRecord(value);
|
|
517
|
+
const out = {};
|
|
518
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
519
|
+
const record = asRecord(spec);
|
|
520
|
+
if (!record)
|
|
521
|
+
continue;
|
|
522
|
+
if (record.type === "provider-defined")
|
|
523
|
+
continue;
|
|
524
|
+
out[name] = {
|
|
525
|
+
type: record.type === "function" ? "function" : undefined,
|
|
526
|
+
description: asString(record.description) || undefined,
|
|
527
|
+
inputSchema: record.inputSchema,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
function formatCodexToolOutput(value) {
|
|
533
|
+
if (typeof value === "string")
|
|
534
|
+
return value;
|
|
535
|
+
try {
|
|
536
|
+
return JSON.stringify(toJsonSafe(value) ?? value);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return String(value);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function executeCodexDynamicToolCall(args, params) {
|
|
543
|
+
const actionDetails = readCodexDynamicActionDetails(params);
|
|
544
|
+
const toolName = asString(actionDetails.actionName).trim();
|
|
545
|
+
const callId = asString(actionDetails.actionCallId).trim();
|
|
546
|
+
const action = toolName ? (args.actions ?? {})[toolName] : undefined;
|
|
547
|
+
const input = actionDetails.input ?? {};
|
|
548
|
+
if (!toolName || !isCodexExecutableAction(action)) {
|
|
549
|
+
const errorText = `codex_dynamic_tool_not_found:${toolName || "unknown"}`;
|
|
550
|
+
return {
|
|
551
|
+
success: false,
|
|
552
|
+
output: { error: errorText },
|
|
553
|
+
errorText,
|
|
554
|
+
response: {
|
|
555
|
+
success: false,
|
|
556
|
+
contentItems: [{ type: "inputText", text: errorText }],
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
const output = await action.execute(input, {
|
|
562
|
+
runtime: args.runtime,
|
|
563
|
+
context: args.storedContext ?? args.context,
|
|
564
|
+
contextIdentifier: args.contextIdentifier,
|
|
565
|
+
toolCallId: callId || undefined,
|
|
566
|
+
messages: [],
|
|
567
|
+
eventId: args.eventId,
|
|
568
|
+
executionId: args.executionId,
|
|
569
|
+
triggerEventId: undefined,
|
|
570
|
+
contextId: args.contextId,
|
|
571
|
+
stepId: args.stepId,
|
|
572
|
+
iteration: args.iteration ?? 0,
|
|
573
|
+
});
|
|
574
|
+
return {
|
|
575
|
+
success: true,
|
|
576
|
+
output,
|
|
577
|
+
response: {
|
|
578
|
+
success: true,
|
|
579
|
+
contentItems: [{ type: "inputText", text: formatCodexToolOutput(output) }],
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
const errorText = error instanceof Error ? error.message : String(error);
|
|
585
|
+
return {
|
|
586
|
+
success: false,
|
|
587
|
+
output: { error: errorText },
|
|
588
|
+
errorText,
|
|
589
|
+
response: {
|
|
590
|
+
success: false,
|
|
591
|
+
contentItems: [{ type: "inputText", text: `Action failed: ${errorText}` }],
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function codexAppServerRespond(baseUrl, payload) {
|
|
597
|
+
const response = await fetch(`${baseUrl}/respond`, {
|
|
598
|
+
method: "POST",
|
|
599
|
+
headers: { "content-type": "application/json" },
|
|
600
|
+
body: JSON.stringify(payload),
|
|
601
|
+
});
|
|
602
|
+
const body = await readJsonResponse(response);
|
|
603
|
+
if (!response.ok || body.error) {
|
|
604
|
+
throw new Error(asString(body.error) || `codex_respond_http_${response.status}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function codexSandboxBridgeScript() {
|
|
608
|
+
return String.raw `
|
|
609
|
+
import http from "node:http";
|
|
610
|
+
import { spawn } from "node:child_process";
|
|
611
|
+
import { createInterface } from "node:readline";
|
|
612
|
+
import { randomUUID } from "node:crypto";
|
|
613
|
+
|
|
614
|
+
const PORT = Number(process.env.CODEX_BRIDGE_PORT || "4500");
|
|
615
|
+
function asRecord(value) { return value && typeof value === "object" ? value : {}; }
|
|
616
|
+
function asString(value) { return typeof value === "string" ? value : value == null ? "" : String(value); }
|
|
617
|
+
const child = spawn("codex", ["app-server", "--enable", "apps"], { stdio: ["pipe", "pipe", "inherit"], env: process.env });
|
|
618
|
+
const rl = createInterface({ input: child.stdout });
|
|
619
|
+
const pending = new Map();
|
|
620
|
+
const subscribers = new Set();
|
|
621
|
+
let initialized = false;
|
|
622
|
+
|
|
623
|
+
function notifyAll(payload) {
|
|
624
|
+
const data = "data: " + JSON.stringify(payload) + "\n\n";
|
|
625
|
+
for (const res of subscribers) {
|
|
626
|
+
try { res.write(data); } catch { subscribers.delete(res); }
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
rl.on("line", (line) => {
|
|
630
|
+
let msg;
|
|
631
|
+
try { msg = JSON.parse(line); } catch { return; }
|
|
632
|
+
if (msg && msg.id && pending.has(msg.id)) {
|
|
633
|
+
const p = pending.get(msg.id);
|
|
634
|
+
pending.delete(msg.id);
|
|
635
|
+
clearTimeout(p.timer);
|
|
636
|
+
if (msg.error) {
|
|
637
|
+
const err = asRecord(msg.error);
|
|
638
|
+
p.reject(new Error(asString(err.message) || asString(msg.error) || "rpc_error"));
|
|
639
|
+
} else {
|
|
640
|
+
p.resolve(msg);
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
notifyAll(msg);
|
|
645
|
+
});
|
|
646
|
+
function sendRpc(payload, timeoutMs = 60000) {
|
|
647
|
+
const id = payload.id || randomUUID();
|
|
648
|
+
const msg = { ...payload, id };
|
|
649
|
+
return new Promise((resolve, reject) => {
|
|
650
|
+
const timer = setTimeout(() => {
|
|
651
|
+
pending.delete(id);
|
|
652
|
+
reject(new Error("rpc_timeout:" + asString(payload.method)));
|
|
653
|
+
}, timeoutMs);
|
|
654
|
+
pending.set(id, { resolve, reject, timer });
|
|
655
|
+
child.stdin.write(JSON.stringify(msg) + "\n");
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
async function ensureInitialized() {
|
|
659
|
+
if (initialized) return;
|
|
660
|
+
await sendRpc({ method: "initialize", params: { clientInfo: { name: "ekairos-sandbox", version: "1.0.0" }, capabilities: { experimentalApi: true } } });
|
|
661
|
+
child.stdin.write(JSON.stringify({ method: "initialized", params: {} }) + "\n");
|
|
662
|
+
initialized = true;
|
|
663
|
+
}
|
|
664
|
+
const server = http.createServer((req, res) => {
|
|
665
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
666
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
667
|
+
res.end(JSON.stringify({ ok: true, initialized }));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (req.method === "GET" && req.url === "/events") {
|
|
671
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
672
|
+
res.write("data: " + JSON.stringify({ type: "ready" }) + "\n\n");
|
|
673
|
+
subscribers.add(res);
|
|
674
|
+
req.on("close", () => subscribers.delete(res));
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (req.method === "POST" && req.url === "/rpc") {
|
|
678
|
+
let body = "";
|
|
679
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
680
|
+
req.on("end", async () => {
|
|
681
|
+
try {
|
|
682
|
+
await ensureInitialized();
|
|
683
|
+
const payload = body ? JSON.parse(body) : {};
|
|
684
|
+
const out = await sendRpc(payload);
|
|
685
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
686
|
+
res.end(JSON.stringify(out));
|
|
687
|
+
} catch (error) {
|
|
688
|
+
res.writeHead(503, { "content-type": "application/json" });
|
|
689
|
+
res.end(JSON.stringify({ error: asString(error?.message || error) }));
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (req.method === "POST" && req.url === "/respond") {
|
|
695
|
+
let body = "";
|
|
696
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
697
|
+
req.on("end", async () => {
|
|
698
|
+
try {
|
|
699
|
+
const payload = body ? JSON.parse(body) : {};
|
|
700
|
+
if (!payload || payload.id === undefined || payload.id === null) {
|
|
701
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
702
|
+
res.end(JSON.stringify({ error: "response_id_required" }));
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
child.stdin.write(JSON.stringify(payload) + "\n");
|
|
706
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
707
|
+
res.end(JSON.stringify({ ok: true }));
|
|
708
|
+
} catch (error) {
|
|
709
|
+
res.writeHead(503, { "content-type": "application/json" });
|
|
710
|
+
res.end(JSON.stringify({ error: asString(error?.message || error) }));
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
716
|
+
res.end(JSON.stringify({ error: "not_found" }));
|
|
717
|
+
});
|
|
718
|
+
child.on("exit", () => process.exit(1));
|
|
719
|
+
server.listen(PORT, "0.0.0.0", async () => {
|
|
720
|
+
try { await ensureInitialized(); } catch {}
|
|
721
|
+
console.log("[codex-bridge] listening http://0.0.0.0:" + PORT);
|
|
722
|
+
});
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
function codexSandboxTurnRunnerScript() {
|
|
726
|
+
return String.raw `
|
|
727
|
+
import { readFileSync } from "node:fs";
|
|
728
|
+
const baseUrl = (process.env.CODEX_BRIDGE_URL || "http://127.0.0.1:4500").replace(/\/+$/, "");
|
|
729
|
+
const instruction = process.env.CODEX_INSTRUCTION_FILE
|
|
730
|
+
? readFileSync(process.env.CODEX_INSTRUCTION_FILE, "utf8")
|
|
731
|
+
: process.env.CODEX_INSTRUCTION || "";
|
|
732
|
+
const repoPath = process.env.CODEX_REPO_PATH || process.cwd();
|
|
733
|
+
const providerContextId = process.env.CODEX_PROVIDER_CONTEXT_ID || "";
|
|
734
|
+
const model = process.env.CODEX_MODEL || "";
|
|
735
|
+
function asRecord(value) { return value && typeof value === "object" ? value : {}; }
|
|
736
|
+
function asString(value) { return typeof value === "string" ? value : value == null ? "" : String(value); }
|
|
737
|
+
function emit(prefix, payload) { process.stdout.write(prefix + "\t" + JSON.stringify(payload) + "\n"); }
|
|
738
|
+
async function rpc(method, params) {
|
|
739
|
+
const res = await fetch(baseUrl + "/rpc", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ method, params }) });
|
|
740
|
+
const json = await res.json().catch(() => ({}));
|
|
741
|
+
if (!res.ok || json.error) throw new Error(asString(json.error) || "rpc_failed:" + method);
|
|
742
|
+
return json;
|
|
743
|
+
}
|
|
744
|
+
function parseSse(block) {
|
|
745
|
+
const lines = block.split("\n").map((line) => line.trimEnd()).filter((line) => line.startsWith("data:"));
|
|
746
|
+
if (!lines.length) return null;
|
|
747
|
+
return lines.map((line) => line.replace(/^data:\s*/, "")).join("\n");
|
|
748
|
+
}
|
|
749
|
+
let threadId = providerContextId;
|
|
750
|
+
let turnId = "";
|
|
751
|
+
let assistantText = "";
|
|
752
|
+
let reasoningText = "";
|
|
753
|
+
let diff = "";
|
|
754
|
+
let usage = {};
|
|
755
|
+
let completedTurn = {};
|
|
756
|
+
const eventsResponse = await fetch(baseUrl + "/events", { headers: { accept: "text/event-stream" } });
|
|
757
|
+
if (!eventsResponse.ok || !eventsResponse.body) throw new Error("events_unavailable:" + eventsResponse.status);
|
|
758
|
+
if (threadId) {
|
|
759
|
+
await rpc("thread/resume", { threadId });
|
|
760
|
+
} else {
|
|
761
|
+
const params = { cwd: repoPath, approvalPolicy: "never", sandboxPolicy: { type: "externalSandbox", networkAccess: "enabled" } };
|
|
762
|
+
if (model) params.model = model;
|
|
763
|
+
const started = await rpc("thread/start", params);
|
|
764
|
+
threadId = asString(asRecord(asRecord(started.result).thread).id) || asString(asRecord(started.result).id) || asString(started.threadId);
|
|
765
|
+
}
|
|
766
|
+
if (!threadId) throw new Error("thread_id_missing");
|
|
767
|
+
const reader = eventsResponse.body.getReader();
|
|
768
|
+
const decoder = new TextDecoder();
|
|
769
|
+
let buffer = "";
|
|
770
|
+
const turnParams = { threadId, input: [{ type: "text", text: instruction }], cwd: repoPath, approvalPolicy: "never", sandboxPolicy: { type: "externalSandbox", networkAccess: "enabled" } };
|
|
771
|
+
if (model) turnParams.model = model;
|
|
772
|
+
const turnStart = await rpc("turn/start", turnParams);
|
|
773
|
+
turnId = asString(asRecord(asRecord(turnStart.result).turn).id) || asString(asRecord(turnStart.result).id) || asString(turnStart.turnId);
|
|
774
|
+
let done = false;
|
|
775
|
+
while (!done) {
|
|
776
|
+
const read = await reader.read();
|
|
777
|
+
if (read.done) break;
|
|
778
|
+
buffer += decoder.decode(read.value, { stream: true });
|
|
779
|
+
const blocks = buffer.split("\n\n");
|
|
780
|
+
buffer = blocks.pop() || "";
|
|
781
|
+
for (const block of blocks) {
|
|
782
|
+
const data = parseSse(block);
|
|
783
|
+
if (!data || data === "[DONE]") continue;
|
|
784
|
+
const evt = JSON.parse(data);
|
|
785
|
+
const method = asString(evt.method);
|
|
786
|
+
if (!method || method.startsWith("codex/event/")) continue;
|
|
787
|
+
const params = asRecord(evt.params);
|
|
788
|
+
const evtTurnId = asString(params.turnId) || asString(asRecord(params.turn).id);
|
|
789
|
+
const evtThreadId = asString(params.threadId) || asString(asRecord(params.turn).threadId);
|
|
790
|
+
const scoped = (evtTurnId && turnId && evtTurnId === turnId) || (evtThreadId && evtThreadId === threadId) || method.startsWith("thread/") || method.startsWith("context/");
|
|
791
|
+
if (!scoped) continue;
|
|
792
|
+
emit("EKAIROS_CODEX_EVENT", evt);
|
|
793
|
+
if (method === "turn/started" && !turnId) turnId = evtTurnId || asString(asRecord(params.turn).id);
|
|
794
|
+
if (method === "item/agentMessage/delta") assistantText += asString(params.delta);
|
|
795
|
+
if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") reasoningText += asString(params.delta);
|
|
796
|
+
if (method === "turn/diff/updated") diff = asString(params.diff);
|
|
797
|
+
if (method === "thread/tokenUsage/updated" || method === "context/tokenUsage/updated") usage = asRecord(params.tokenUsage);
|
|
798
|
+
if (method === "item/completed") {
|
|
799
|
+
const item = asRecord(params.item);
|
|
800
|
+
if (asString(item.type) === "agentMessage" && asString(item.text).trim()) assistantText = asString(item.text);
|
|
801
|
+
if (asString(item.type) === "reasoning" && asString(item.summary).trim()) reasoningText = asString(item.summary);
|
|
802
|
+
}
|
|
803
|
+
if (method === "turn/completed") {
|
|
804
|
+
completedTurn = asRecord(params.turn);
|
|
805
|
+
done = true;
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
if (method === "turn/failed") throw new Error("turn_failed:" + (evtTurnId || turnId || "unknown"));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
await reader.cancel().catch(() => {});
|
|
812
|
+
emit("EKAIROS_CODEX_RESULT", { providerContextId: threadId, turnId: asString(completedTurn.id) || turnId, assistantText, reasoningText, diff, usage, completedTurn });
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
815
|
+
function ensureOk(result, label) {
|
|
816
|
+
if (!result.ok)
|
|
817
|
+
throw new Error(`${label}: ${result.error}`);
|
|
818
|
+
return result.data;
|
|
819
|
+
}
|
|
820
|
+
async function resolveCodexSandboxDomain(runtime) {
|
|
821
|
+
const rootDomain = runtime.meta?.().domain;
|
|
822
|
+
if (!rootDomain) {
|
|
823
|
+
throw new Error("codex_sandbox_domain_required");
|
|
824
|
+
}
|
|
825
|
+
const scoped = await runtime.use(rootDomain);
|
|
826
|
+
if (!scoped.actions) {
|
|
827
|
+
throw new Error("codex_sandbox_actions_required");
|
|
828
|
+
}
|
|
829
|
+
return scoped;
|
|
830
|
+
}
|
|
831
|
+
async function executeCodexSandboxTurn(args, helpers) {
|
|
832
|
+
const sandboxConfig = args.config.sandbox ?? {};
|
|
833
|
+
const runtime = args.runtime;
|
|
834
|
+
if (!isCodexSandboxRuntime(runtime)) {
|
|
835
|
+
throw new Error("codex_sandbox_runtime_required");
|
|
836
|
+
}
|
|
837
|
+
const scoped = await resolveCodexSandboxDomain(runtime);
|
|
838
|
+
const actions = scoped.actions;
|
|
839
|
+
const sandboxDb = scoped.db;
|
|
840
|
+
const provider = sandboxConfig.provider ?? "sprites";
|
|
841
|
+
const homeDir = provider === "vercel" ? "/vercel/sandbox" : "/home/sprite";
|
|
842
|
+
const codexHome = String(sandboxConfig.codexHome ?? `${homeDir}/.codex`).trim() || `${homeDir}/.codex`;
|
|
843
|
+
const bridgePort = Math.max(1, Number(sandboxConfig.bridgePort ?? 4500));
|
|
844
|
+
const appPort = Math.max(1, Number(sandboxConfig.appPort ?? (provider === "vercel" ? 3000 : 8080)));
|
|
845
|
+
const defaultWorkspaceRoot = provider === "vercel" ? "/vercel/sandbox" : "/workspace";
|
|
846
|
+
const repoPath = String(args.config.repoPath || `${defaultWorkspaceRoot}/ekairos-app`).trim() ||
|
|
847
|
+
`${defaultWorkspaceRoot}/ekairos-app`;
|
|
848
|
+
const workRoot = `${defaultWorkspaceRoot}/.ekairos/codex`;
|
|
849
|
+
const bridgePath = `${workRoot}/codex-bridge.mjs`;
|
|
850
|
+
const turnRunnerPath = `${workRoot}/codex-turn-runner.mjs`;
|
|
851
|
+
const instructionPath = `${workRoot}/instruction-${args.executionId}-${args.stepId}.txt`;
|
|
852
|
+
const checkpoints = [];
|
|
853
|
+
const observedCommandProcesses = new Map();
|
|
854
|
+
const observedCommandProcessResults = new Map();
|
|
855
|
+
let sandboxId = String(sandboxConfig.sandboxId ?? "").trim();
|
|
856
|
+
if (!sandboxId) {
|
|
857
|
+
const created = ensureOk(await actions.createSandbox({
|
|
858
|
+
provider: sandboxConfig.provider ?? "sprites",
|
|
859
|
+
runtime: sandboxConfig.runtime ?? "node22",
|
|
860
|
+
purpose: sandboxConfig.purpose ?? "codex-reactor-sandbox",
|
|
861
|
+
ports: Array.from(new Set([bridgePort, appPort, ...(Array.isArray(sandboxConfig.ports) ? sandboxConfig.ports : [])])),
|
|
862
|
+
...(provider === "sprites"
|
|
863
|
+
? {
|
|
864
|
+
sprites: {
|
|
865
|
+
name: sandboxConfig.spriteName,
|
|
866
|
+
waitForCapacity: true,
|
|
867
|
+
urlSettings: { auth: "public" },
|
|
868
|
+
deleteOnStop: true,
|
|
869
|
+
},
|
|
870
|
+
}
|
|
871
|
+
: {}),
|
|
872
|
+
...(provider === "vercel" ? { vercel: sandboxConfig.vercel ?? {} } : {}),
|
|
873
|
+
}), "codex_sandbox_create");
|
|
874
|
+
sandboxId = String(created.sandboxId);
|
|
875
|
+
}
|
|
876
|
+
if (!sandboxId)
|
|
877
|
+
throw new Error("codex_sandbox_id_missing");
|
|
878
|
+
const emitAndObserveProviderChunk = async (providerChunk) => {
|
|
879
|
+
await helpers.emitProviderChunk(providerChunk);
|
|
880
|
+
const evt = asRecord(providerChunk);
|
|
881
|
+
const method = asString(evt.method);
|
|
882
|
+
const params = asRecord(evt.params);
|
|
883
|
+
if (!method)
|
|
884
|
+
return;
|
|
885
|
+
if (method === "item/started") {
|
|
886
|
+
const item = asRecord(params.item);
|
|
887
|
+
if (asString(item.type) !== "commandExecution")
|
|
888
|
+
return;
|
|
889
|
+
const codexItemId = asString(item.id);
|
|
890
|
+
if (!codexItemId || observedCommandProcesses.has(codexItemId))
|
|
891
|
+
return;
|
|
892
|
+
if (!hasStreamCapableSandboxDb(sandboxDb))
|
|
893
|
+
return;
|
|
894
|
+
const processId = randomUUID();
|
|
895
|
+
const streamClientId = `sandbox-process:${processId}`;
|
|
896
|
+
const stream = sandboxDb.streams.createWriteStream({ clientId: streamClientId });
|
|
897
|
+
const streamId = typeof stream.streamId === "function" ? await stream.streamId() : streamClientId;
|
|
898
|
+
const writer = stream.getWriter();
|
|
899
|
+
const now = Date.now();
|
|
900
|
+
const metadata = {
|
|
901
|
+
source: "codex.commandExecution",
|
|
902
|
+
codexItemId,
|
|
903
|
+
providerThreadId: asString(params.threadId),
|
|
904
|
+
providerTurnId: asString(params.turnId),
|
|
905
|
+
parent: "codex-app-server",
|
|
906
|
+
commandActions: item.commandActions,
|
|
907
|
+
observed: true,
|
|
908
|
+
lastSeq: 1,
|
|
909
|
+
chunkCount: 1,
|
|
910
|
+
};
|
|
911
|
+
await sandboxDb.transact([
|
|
912
|
+
sandboxDb.tx.sandbox_processes[processId]
|
|
913
|
+
.update({
|
|
914
|
+
kind: "command",
|
|
915
|
+
mode: "foreground",
|
|
916
|
+
status: "running",
|
|
917
|
+
provider,
|
|
918
|
+
command: asString(item.command),
|
|
919
|
+
args: [],
|
|
920
|
+
cwd: asString(item.cwd) || repoPath,
|
|
921
|
+
externalProcessId: asString(item.processId) || undefined,
|
|
922
|
+
streamId,
|
|
923
|
+
streamClientId,
|
|
924
|
+
streamStartedAt: now,
|
|
925
|
+
startedAt: now,
|
|
926
|
+
updatedAt: now,
|
|
927
|
+
metadata,
|
|
928
|
+
})
|
|
929
|
+
.link({ sandbox: sandboxId, stream: streamId }),
|
|
930
|
+
]);
|
|
931
|
+
const statusChunk = {
|
|
932
|
+
version: 1,
|
|
933
|
+
at: new Date().toISOString(),
|
|
934
|
+
seq: 1,
|
|
935
|
+
type: "status",
|
|
936
|
+
sandboxId,
|
|
937
|
+
processId,
|
|
938
|
+
data: {
|
|
939
|
+
status: "running",
|
|
940
|
+
command: asString(item.command),
|
|
941
|
+
args: [],
|
|
942
|
+
cwd: asString(item.cwd) || repoPath,
|
|
943
|
+
externalProcessId: asString(item.processId) || null,
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
await writer.write(`${JSON.stringify(statusChunk)}\n`);
|
|
947
|
+
observedCommandProcesses.set(codexItemId, {
|
|
948
|
+
processId,
|
|
949
|
+
streamId,
|
|
950
|
+
streamClientId,
|
|
951
|
+
writer,
|
|
952
|
+
seq: 1,
|
|
953
|
+
});
|
|
954
|
+
observedCommandProcessResults.set(codexItemId, {
|
|
955
|
+
processId,
|
|
956
|
+
streamId,
|
|
957
|
+
streamClientId,
|
|
958
|
+
});
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (method === "item/commandExecution/outputDelta") {
|
|
962
|
+
const codexItemId = asString(params.itemId);
|
|
963
|
+
const observed = observedCommandProcesses.get(codexItemId);
|
|
964
|
+
if (!observed)
|
|
965
|
+
return;
|
|
966
|
+
observed.seq += 1;
|
|
967
|
+
await observed.writer.write(`${JSON.stringify({
|
|
968
|
+
version: 1,
|
|
969
|
+
at: new Date().toISOString(),
|
|
970
|
+
seq: observed.seq,
|
|
971
|
+
type: "stdout",
|
|
972
|
+
sandboxId,
|
|
973
|
+
processId: observed.processId,
|
|
974
|
+
data: {
|
|
975
|
+
text: asString(params.delta),
|
|
976
|
+
source: "codex.commandExecution",
|
|
977
|
+
codexItemId,
|
|
978
|
+
},
|
|
979
|
+
})}\n`);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (method === "item/completed") {
|
|
983
|
+
const item = asRecord(params.item);
|
|
984
|
+
if (asString(item.type) !== "commandExecution")
|
|
985
|
+
return;
|
|
986
|
+
const codexItemId = asString(item.id);
|
|
987
|
+
const observed = observedCommandProcesses.get(codexItemId);
|
|
988
|
+
if (!observed)
|
|
989
|
+
return;
|
|
990
|
+
const aggregatedOutput = asString(item.aggregatedOutput);
|
|
991
|
+
if (aggregatedOutput) {
|
|
992
|
+
observed.seq += 1;
|
|
993
|
+
await observed.writer.write(`${JSON.stringify({
|
|
994
|
+
version: 1,
|
|
995
|
+
at: new Date().toISOString(),
|
|
996
|
+
seq: observed.seq,
|
|
997
|
+
type: "stdout",
|
|
998
|
+
sandboxId,
|
|
999
|
+
processId: observed.processId,
|
|
1000
|
+
data: {
|
|
1001
|
+
text: aggregatedOutput,
|
|
1002
|
+
source: "codex.commandExecution",
|
|
1003
|
+
codexItemId,
|
|
1004
|
+
aggregated: true,
|
|
1005
|
+
},
|
|
1006
|
+
})}\n`);
|
|
1007
|
+
}
|
|
1008
|
+
const exitCode = typeof item.exitCode === "number" ? item.exitCode : Number(item.exitCode ?? 0);
|
|
1009
|
+
const status = asString(item.status) === "failed" || exitCode !== 0 ? "failed" : "exited";
|
|
1010
|
+
observed.seq += 1;
|
|
1011
|
+
await observed.writer.write(`${JSON.stringify({
|
|
1012
|
+
version: 1,
|
|
1013
|
+
at: new Date().toISOString(),
|
|
1014
|
+
seq: observed.seq,
|
|
1015
|
+
type: "exit",
|
|
1016
|
+
sandboxId,
|
|
1017
|
+
processId: observed.processId,
|
|
1018
|
+
data: {
|
|
1019
|
+
exitCode: Number.isFinite(exitCode) ? exitCode : null,
|
|
1020
|
+
status,
|
|
1021
|
+
},
|
|
1022
|
+
})}\n`);
|
|
1023
|
+
await observed.writer.close();
|
|
1024
|
+
observed.writer.releaseLock();
|
|
1025
|
+
await sandboxDb.transact([
|
|
1026
|
+
sandboxDb.tx.sandbox_processes[observed.processId].update({
|
|
1027
|
+
status,
|
|
1028
|
+
...(Number.isFinite(exitCode) ? { exitCode } : {}),
|
|
1029
|
+
streamFinishedAt: Date.now(),
|
|
1030
|
+
streamAbortReason: asString(item.error) || null,
|
|
1031
|
+
exitedAt: Date.now(),
|
|
1032
|
+
updatedAt: Date.now(),
|
|
1033
|
+
metadata: {
|
|
1034
|
+
source: "codex.commandExecution",
|
|
1035
|
+
codexItemId,
|
|
1036
|
+
providerThreadId: asString(params.threadId),
|
|
1037
|
+
providerTurnId: asString(params.turnId),
|
|
1038
|
+
durationMs: item.durationMs,
|
|
1039
|
+
completed: item,
|
|
1040
|
+
},
|
|
1041
|
+
}),
|
|
1042
|
+
]);
|
|
1043
|
+
observedCommandProcesses.delete(codexItemId);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
ensureOk(await actions.installCodexAuth({
|
|
1047
|
+
sandboxId,
|
|
1048
|
+
codexHome,
|
|
1049
|
+
authJsonPath: sandboxConfig.authJsonPath,
|
|
1050
|
+
credentialsJsonPath: sandboxConfig.credentialsJsonPath,
|
|
1051
|
+
configTomlPath: sandboxConfig.configTomlPath,
|
|
1052
|
+
}), "codex_sandbox_auth");
|
|
1053
|
+
ensureOk(await actions.writeFiles({
|
|
1054
|
+
sandboxId,
|
|
1055
|
+
files: [
|
|
1056
|
+
{
|
|
1057
|
+
path: bridgePath,
|
|
1058
|
+
contentBase64: Buffer.from(codexSandboxBridgeScript(), "utf8").toString("base64"),
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
path: turnRunnerPath,
|
|
1062
|
+
contentBase64: Buffer.from(codexSandboxTurnRunnerScript(), "utf8").toString("base64"),
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
path: instructionPath,
|
|
1066
|
+
contentBase64: Buffer.from(args.instruction, "utf8").toString("base64"),
|
|
1067
|
+
},
|
|
1068
|
+
],
|
|
1069
|
+
}), "codex_sandbox_write_files");
|
|
1070
|
+
const runProcess = async (label, script, kind = "command", requiredText) => {
|
|
1071
|
+
const result = ensureOk(await actions.runCommandProcess({
|
|
1072
|
+
sandboxId,
|
|
1073
|
+
command: "sh",
|
|
1074
|
+
args: ["-lc", script],
|
|
1075
|
+
kind,
|
|
1076
|
+
mode: "foreground",
|
|
1077
|
+
metadata: { source: "codex-reactor", label },
|
|
1078
|
+
}), label);
|
|
1079
|
+
if (requiredText) {
|
|
1080
|
+
const output = stripProviderControlChars(`${asString(asRecord(result.result).output)}\n${asString(asRecord(result.result).error)}`);
|
|
1081
|
+
if (!output.includes(requiredText)) {
|
|
1082
|
+
throw new Error(`${label}: missing_sentinel:${requiredText}:${output.slice(-1000)}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return result;
|
|
1086
|
+
};
|
|
1087
|
+
await runProcess("codex_sandbox_prepare_codex", [
|
|
1088
|
+
"set -euo pipefail",
|
|
1089
|
+
`mkdir -p ${shellSingleQuote(codexHome)} ${shellSingleQuote(workRoot)}`,
|
|
1090
|
+
`chmod 700 ${shellSingleQuote(codexHome)} || true`,
|
|
1091
|
+
`chmod 600 ${shellSingleQuote(`${codexHome}/auth.json`)} 2>/dev/null || true`,
|
|
1092
|
+
"if ! command -v codex >/dev/null 2>&1; then npm i -g @openai/codex@latest; fi",
|
|
1093
|
+
`HOME=${shellSingleQuote(homeDir)} CODEX_HOME=${shellSingleQuote(codexHome)} codex login status`,
|
|
1094
|
+
"echo codex_sandbox_prepare_codex_ok",
|
|
1095
|
+
].join("\n"), "command", "codex_sandbox_prepare_codex_ok");
|
|
1096
|
+
if (sandboxConfig.checkpoint !== false) {
|
|
1097
|
+
const checkpoint = await actions.createCheckpoint({
|
|
1098
|
+
sandboxId,
|
|
1099
|
+
comment: "codex auth and cli ready",
|
|
1100
|
+
});
|
|
1101
|
+
if (checkpoint.ok) {
|
|
1102
|
+
checkpoints.push({ label: "codex-ready", checkpointId: String(checkpoint.data.checkpointId) });
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
await runProcess("codex_sandbox_start_bridge", [
|
|
1106
|
+
"set -euo pipefail",
|
|
1107
|
+
`if ! curl -fsS http://127.0.0.1:${bridgePort}/health >/dev/null 2>&1; then`,
|
|
1108
|
+
` HOME=${shellSingleQuote(homeDir)} CODEX_HOME=${shellSingleQuote(codexHome)} CODEX_BRIDGE_PORT=${bridgePort} nohup node ${shellSingleQuote(bridgePath)} > /tmp/ekairos-codex-bridge-${bridgePort}.log 2>&1 &`,
|
|
1109
|
+
` echo $! > /tmp/ekairos-codex-bridge-${bridgePort}.pid`,
|
|
1110
|
+
"fi",
|
|
1111
|
+
`for i in $(seq 1 90); do curl -fsS http://127.0.0.1:${bridgePort}/health >/dev/null 2>&1 && echo codex_sandbox_bridge_ok && exit 0; sleep 1; done`,
|
|
1112
|
+
`cat /tmp/ekairos-codex-bridge-${bridgePort}.log || true`,
|
|
1113
|
+
"exit 1",
|
|
1114
|
+
].join("\n"), "codex-app-server", "codex_sandbox_bridge_ok");
|
|
1115
|
+
if (sandboxConfig.createApp) {
|
|
1116
|
+
const createdApp = ensureOk(await actions.createEkairosApp({
|
|
1117
|
+
sandboxId,
|
|
1118
|
+
appDir: repoPath,
|
|
1119
|
+
packageManager: "pnpm",
|
|
1120
|
+
instantTokenEnvName: "INSTANT_PERSONAL_ACCESS_TOKEN",
|
|
1121
|
+
}), "codex_sandbox_create_app");
|
|
1122
|
+
const createdAppOutput = stripProviderControlChars(asString(asRecord(createdApp.result).output));
|
|
1123
|
+
if (!createdAppOutput.includes("sandbox_create_ekairos_app_ok")) {
|
|
1124
|
+
throw new Error(`codex_sandbox_create_app: missing_sentinel:${createdAppOutput.slice(-1000)}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (sandboxConfig.installApp) {
|
|
1128
|
+
await runProcess("codex_sandbox_install_app", [
|
|
1129
|
+
"set -euo pipefail",
|
|
1130
|
+
`cd ${shellSingleQuote(repoPath)}`,
|
|
1131
|
+
"for i in 1 2 3; do npx -y pnpm@10.15.1 install && break; echo pnpm_install_retry_$i; sleep 20; done",
|
|
1132
|
+
"test -x node_modules/.bin/next",
|
|
1133
|
+
"echo codex_sandbox_install_app_ok",
|
|
1134
|
+
].join("\n"), "command", "codex_sandbox_install_app_ok");
|
|
1135
|
+
}
|
|
1136
|
+
let appBaseUrl = "";
|
|
1137
|
+
if (sandboxConfig.startApp) {
|
|
1138
|
+
await runProcess("codex_sandbox_start_app", [
|
|
1139
|
+
"set -euo pipefail",
|
|
1140
|
+
`cd ${shellSingleQuote(repoPath)}`,
|
|
1141
|
+
`if ! curl -fsS http://127.0.0.1:${appPort}/api/ekairos/domain >/dev/null 2>&1; then`,
|
|
1142
|
+
` nohup npx -y pnpm@10.15.1 dev --hostname 0.0.0.0 --port ${appPort} > /tmp/ekairos-app-${appPort}.log 2>&1 &`,
|
|
1143
|
+
` echo $! > /tmp/ekairos-app-${appPort}.pid`,
|
|
1144
|
+
"fi",
|
|
1145
|
+
`for i in $(seq 1 180); do curl -fsS http://127.0.0.1:${appPort}/api/ekairos/domain >/dev/null 2>&1 && echo codex_sandbox_start_app_ok && exit 0; sleep 1; done`,
|
|
1146
|
+
`cat /tmp/ekairos-app-${appPort}.log || true`,
|
|
1147
|
+
"exit 1",
|
|
1148
|
+
].join("\n"), "dev-server", "codex_sandbox_start_app_ok");
|
|
1149
|
+
const portUrl = ensureOk(await actions.getPortUrl({ sandboxId, port: appPort }), "codex_sandbox_port_url");
|
|
1150
|
+
appBaseUrl = String(portUrl.url ?? "").replace(/\/+$/, "");
|
|
1151
|
+
if (appBaseUrl) {
|
|
1152
|
+
const response = await fetch(`${appBaseUrl}/api/ekairos/domain`);
|
|
1153
|
+
if (!response.ok)
|
|
1154
|
+
throw new Error(`codex_sandbox_app_url_unavailable_${response.status}`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (sandboxConfig.checkpoint !== false && (sandboxConfig.createApp || sandboxConfig.installApp || sandboxConfig.startApp)) {
|
|
1158
|
+
const checkpoint = await actions.createCheckpoint({
|
|
1159
|
+
sandboxId,
|
|
1160
|
+
comment: "codex reactor app ready",
|
|
1161
|
+
});
|
|
1162
|
+
if (checkpoint.ok) {
|
|
1163
|
+
checkpoints.push({ label: "app-ready", checkpointId: String(checkpoint.data.checkpointId) });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const bridgeUrl = ensureOk(await actions.getPortUrl({ sandboxId, port: bridgePort }), "codex_sandbox_bridge_url");
|
|
1167
|
+
const bridgeBaseUrl = String(bridgeUrl.url ?? "").replace(/\/+$/, "");
|
|
1168
|
+
if (!bridgeBaseUrl)
|
|
1169
|
+
throw new Error("codex_sandbox_bridge_url_missing");
|
|
1170
|
+
const turn = await executeCodexHttpTurn({
|
|
1171
|
+
...args,
|
|
1172
|
+
systemPrompt: args.systemPrompt,
|
|
1173
|
+
config: {
|
|
1174
|
+
...args.config,
|
|
1175
|
+
mode: "remote",
|
|
1176
|
+
appServerUrl: bridgeBaseUrl,
|
|
1177
|
+
repoPath,
|
|
1178
|
+
},
|
|
1179
|
+
actions: args.actions,
|
|
1180
|
+
actionSpecs: args.actionSpecs,
|
|
1181
|
+
context: args.context,
|
|
1182
|
+
storedContext: args.storedContext,
|
|
1183
|
+
contextIdentifier: args.contextIdentifier,
|
|
1184
|
+
}, {
|
|
1185
|
+
...helpers,
|
|
1186
|
+
emitProviderChunk: emitAndObserveProviderChunk,
|
|
1187
|
+
}, bridgeBaseUrl);
|
|
1188
|
+
return {
|
|
1189
|
+
providerContextId: turn.providerContextId,
|
|
1190
|
+
turnId: turn.turnId,
|
|
1191
|
+
assistantText: turn.assistantText,
|
|
1192
|
+
reasoningText: turn.reasoningText,
|
|
1193
|
+
diff: turn.diff,
|
|
1194
|
+
toolParts: turn.toolParts,
|
|
1195
|
+
usage: turn.usage,
|
|
1196
|
+
metadata: {
|
|
1197
|
+
provider: "codex-sandbox",
|
|
1198
|
+
dynamicTools: asUnknownArray(asRecord(turn.metadata).dynamicTools),
|
|
1199
|
+
sandbox: {
|
|
1200
|
+
sandboxId,
|
|
1201
|
+
repoPath,
|
|
1202
|
+
appBaseUrl,
|
|
1203
|
+
bridgeBaseUrl,
|
|
1204
|
+
bridgePort,
|
|
1205
|
+
appPort,
|
|
1206
|
+
processId: "",
|
|
1207
|
+
streamId: "",
|
|
1208
|
+
streamClientId: "",
|
|
1209
|
+
commandProcesses: Object.fromEntries(observedCommandProcessResults.entries()),
|
|
1210
|
+
checkpoints,
|
|
1211
|
+
},
|
|
1212
|
+
providerResponse: asRecord(turn.metadata).providerResponse,
|
|
1213
|
+
streamTrace: helpers.streamTrace(),
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
async function readJsonResponse(response) {
|
|
1218
|
+
const text = await response.text().catch(() => "");
|
|
1219
|
+
if (!text.trim())
|
|
1220
|
+
return {};
|
|
1221
|
+
try {
|
|
1222
|
+
return asRecord(JSON.parse(text));
|
|
1223
|
+
}
|
|
1224
|
+
catch {
|
|
1225
|
+
return {};
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
async function codexAppServerRpc(baseUrl, method, params) {
|
|
1229
|
+
const response = await fetch(`${baseUrl}/rpc`, {
|
|
1230
|
+
method: "POST",
|
|
1231
|
+
headers: { "content-type": "application/json" },
|
|
1232
|
+
body: JSON.stringify({ method, params }),
|
|
1233
|
+
});
|
|
1234
|
+
const payload = await readJsonResponse(response);
|
|
1235
|
+
if (!response.ok) {
|
|
1236
|
+
const error = asString(payload.error) || asString(asRecord(payload.error).message);
|
|
1237
|
+
throw new Error(error || `codex_rpc_http_${response.status}`);
|
|
1238
|
+
}
|
|
1239
|
+
if (payload.error) {
|
|
1240
|
+
const error = asString(payload.error) || asString(asRecord(payload.error).message);
|
|
1241
|
+
throw new Error(error || "codex_rpc_error");
|
|
1242
|
+
}
|
|
1243
|
+
return payload;
|
|
1244
|
+
}
|
|
1245
|
+
async function executeCodexHttpTurn(args, helpers, baseUrl) {
|
|
1246
|
+
const eventsResponse = await fetch(`${baseUrl}/events`, {
|
|
1247
|
+
method: "GET",
|
|
1248
|
+
headers: { accept: "text/event-stream" },
|
|
1249
|
+
});
|
|
1250
|
+
if (!eventsResponse.ok || !eventsResponse.body) {
|
|
1251
|
+
throw new Error(`codex_events_unavailable_${eventsResponse.status}`);
|
|
1252
|
+
}
|
|
1253
|
+
const dynamicTools = buildCodexDynamicTools(args.actionSpecs);
|
|
1254
|
+
const baseInstructions = asString(args.systemPrompt).trim();
|
|
1255
|
+
const requestedThreadId = asString(args.config.providerContextId).trim();
|
|
1256
|
+
let providerContextId = requestedThreadId;
|
|
1257
|
+
if (providerContextId && isValidProviderContextId(providerContextId)) {
|
|
1258
|
+
await codexAppServerRpc(baseUrl, "thread/resume", { threadId: providerContextId });
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
const startParams = {
|
|
1262
|
+
cwd: args.config.repoPath,
|
|
1263
|
+
approvalPolicy: args.config.approvalPolicy ?? "never",
|
|
1264
|
+
sandboxPolicy: args.config.sandboxPolicy && Object.keys(args.config.sandboxPolicy).length > 0
|
|
1265
|
+
? args.config.sandboxPolicy
|
|
1266
|
+
: { type: "externalSandbox", networkAccess: "enabled" },
|
|
1267
|
+
...(dynamicTools.length > 0 ? { dynamicTools, dynamic_tools: dynamicTools } : {}),
|
|
1268
|
+
...(dynamicTools.length > 0
|
|
1269
|
+
? { experimentalRawEvents: true, persistExtendedHistory: true }
|
|
1270
|
+
: {}),
|
|
1271
|
+
...(baseInstructions ? { baseInstructions } : {}),
|
|
1272
|
+
};
|
|
1273
|
+
if (args.config.model)
|
|
1274
|
+
startParams.model = args.config.model;
|
|
1275
|
+
const started = await codexAppServerRpc(baseUrl, "thread/start", startParams);
|
|
1276
|
+
providerContextId =
|
|
1277
|
+
asString(asRecord(asRecord(started.result).thread).id) ||
|
|
1278
|
+
asString(asRecord(started.result).id) ||
|
|
1279
|
+
asString(started.threadId);
|
|
1280
|
+
}
|
|
1281
|
+
if (!providerContextId)
|
|
1282
|
+
throw new Error("codex_thread_id_missing");
|
|
1283
|
+
const turnParams = {
|
|
1284
|
+
threadId: providerContextId,
|
|
1285
|
+
input: [{ type: "text", text: args.instruction }],
|
|
1286
|
+
cwd: args.config.repoPath,
|
|
1287
|
+
approvalPolicy: args.config.approvalPolicy ?? "never",
|
|
1288
|
+
sandboxPolicy: args.config.sandboxPolicy && Object.keys(args.config.sandboxPolicy).length > 0
|
|
1289
|
+
? args.config.sandboxPolicy
|
|
1290
|
+
: { type: "externalSandbox", networkAccess: "enabled" },
|
|
1291
|
+
...(dynamicTools.length > 0 ? { dynamicTools, dynamic_tools: dynamicTools } : {}),
|
|
1292
|
+
};
|
|
1293
|
+
if (args.config.model)
|
|
1294
|
+
turnParams.model = args.config.model;
|
|
1295
|
+
const turnStart = await codexAppServerRpc(baseUrl, "turn/start", turnParams);
|
|
1296
|
+
let turnId = asString(asRecord(asRecord(turnStart.result).turn).id) ||
|
|
1297
|
+
asString(asRecord(turnStart.result).id) ||
|
|
1298
|
+
asString(turnStart.turnId);
|
|
1299
|
+
const reader = eventsResponse.body.getReader();
|
|
1300
|
+
const decoder = new TextDecoder();
|
|
1301
|
+
let buffer = "";
|
|
1302
|
+
let assistantText = "";
|
|
1303
|
+
let reasoningText = "";
|
|
1304
|
+
let diff = "";
|
|
1305
|
+
let usage = {};
|
|
1306
|
+
let completedTurn = {};
|
|
1307
|
+
const isScopedToTurn = (evt) => {
|
|
1308
|
+
const params = asRecord(evt.params);
|
|
1309
|
+
const evtTurnId = asString(params.turnId) || asString(asRecord(params.turn).id);
|
|
1310
|
+
const evtThreadId = asString(params.threadId) ||
|
|
1311
|
+
asString(params.providerContextId) ||
|
|
1312
|
+
asString(asRecord(params.turn).threadId) ||
|
|
1313
|
+
asString(asRecord(params.turn).providerContextId);
|
|
1314
|
+
return ((evtTurnId && turnId && evtTurnId === turnId) ||
|
|
1315
|
+
(evtThreadId && evtThreadId === providerContextId) ||
|
|
1316
|
+
asString(evt.method).startsWith("thread/") ||
|
|
1317
|
+
asString(evt.method).startsWith("context/"));
|
|
1318
|
+
};
|
|
1319
|
+
try {
|
|
1320
|
+
while (true) {
|
|
1321
|
+
const read = await reader.read();
|
|
1322
|
+
if (read.done)
|
|
1323
|
+
break;
|
|
1324
|
+
if (!read.value)
|
|
1325
|
+
continue;
|
|
1326
|
+
buffer += decoder.decode(read.value, { stream: true });
|
|
1327
|
+
const blocks = buffer.split("\n\n");
|
|
1328
|
+
buffer = blocks.pop() ?? "";
|
|
1329
|
+
for (const block of blocks) {
|
|
1330
|
+
const data = parseSseDataBlock(block);
|
|
1331
|
+
if (!data || data === "[DONE]")
|
|
1332
|
+
continue;
|
|
1333
|
+
const evt = asRecord(JSON.parse(data));
|
|
1334
|
+
const method = asString(evt.method);
|
|
1335
|
+
if (!method)
|
|
1336
|
+
continue;
|
|
1337
|
+
if (method === "item/tool/call" && evt.id !== undefined && evt.id !== null) {
|
|
1338
|
+
if (!isScopedToTurn(evt))
|
|
1339
|
+
continue;
|
|
1340
|
+
const toolParams = asRecord(evt.params);
|
|
1341
|
+
await helpers.emitProviderChunk(evt);
|
|
1342
|
+
const executed = await executeCodexDynamicToolCall(args, toolParams);
|
|
1343
|
+
await helpers.emitProviderChunk({
|
|
1344
|
+
method: "item/tool/result",
|
|
1345
|
+
params: {
|
|
1346
|
+
...toolParams,
|
|
1347
|
+
result: executed.response,
|
|
1348
|
+
output: executed.output,
|
|
1349
|
+
success: executed.success,
|
|
1350
|
+
errorText: executed.errorText,
|
|
1351
|
+
},
|
|
1352
|
+
});
|
|
1353
|
+
await codexAppServerRespond(baseUrl, {
|
|
1354
|
+
id: evt.id,
|
|
1355
|
+
result: executed.response,
|
|
1356
|
+
});
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
const params = asRecord(evt.params);
|
|
1360
|
+
if (!isScopedToTurn(evt))
|
|
1361
|
+
continue;
|
|
1362
|
+
await helpers.emitProviderChunk(evt);
|
|
1363
|
+
if (method === "turn/started" && !turnId) {
|
|
1364
|
+
turnId = asString(asRecord(params.turn).id) || asString(params.turnId);
|
|
1365
|
+
}
|
|
1366
|
+
if (method === "item/agentMessage/delta") {
|
|
1367
|
+
assistantText += asString(params.delta);
|
|
1368
|
+
}
|
|
1369
|
+
if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
|
|
1370
|
+
reasoningText += asString(params.delta);
|
|
1371
|
+
}
|
|
1372
|
+
if (method === "turn/diff/updated") {
|
|
1373
|
+
diff = asString(params.diff);
|
|
1374
|
+
}
|
|
1375
|
+
if (method === "thread/tokenUsage/updated" || method === "context/tokenUsage/updated") {
|
|
1376
|
+
usage = asRecord(params.tokenUsage);
|
|
1377
|
+
}
|
|
1378
|
+
if (method === "item/completed") {
|
|
1379
|
+
const item = asRecord(params.item);
|
|
1380
|
+
if (asString(item.type) === "agentMessage" && asString(item.text).trim()) {
|
|
1381
|
+
assistantText = asString(item.text);
|
|
1382
|
+
}
|
|
1383
|
+
if (asString(item.type) === "reasoning" && asString(item.summary).trim()) {
|
|
1384
|
+
reasoningText = asString(item.summary);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (method === "turn/completed") {
|
|
1388
|
+
completedTurn = asRecord(params.turn);
|
|
1389
|
+
return {
|
|
1390
|
+
providerContextId,
|
|
1391
|
+
turnId: asString(completedTurn.id) || turnId,
|
|
1392
|
+
assistantText,
|
|
1393
|
+
reasoningText,
|
|
1394
|
+
diff,
|
|
1395
|
+
toolParts: asUnknownArray(completedTurn.toolParts),
|
|
1396
|
+
usage,
|
|
1397
|
+
metadata: {
|
|
1398
|
+
provider: "codex-app-server",
|
|
1399
|
+
providerResponse: completedTurn,
|
|
1400
|
+
dynamicTools: dynamicTools.map((tool) => asString(tool.name)).filter(Boolean),
|
|
1401
|
+
streamTrace: helpers.streamTrace(),
|
|
1402
|
+
},
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
if (method === "turn/failed") {
|
|
1406
|
+
const evtTurnId = asString(params.turnId) || asString(asRecord(params.turn).id);
|
|
1407
|
+
throw new Error(`codex_turn_failed_${evtTurnId || turnId || "unknown"}`);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
finally {
|
|
1413
|
+
await reader.cancel().catch(() => { });
|
|
1414
|
+
}
|
|
1415
|
+
throw new Error("codex_turn_completion_missing");
|
|
1416
|
+
}
|
|
1417
|
+
export async function executeCodexAppServerTurnStep(args) {
|
|
1418
|
+
"use step";
|
|
1419
|
+
const baseUrl = normalizeAppServerBaseUrl(args.config.appServerUrl);
|
|
1420
|
+
if (!baseUrl)
|
|
1421
|
+
throw new Error("codex_app_server_url_required");
|
|
1422
|
+
let sequence = 0;
|
|
1423
|
+
const mappedChunks = [];
|
|
1424
|
+
const chunkTypeCounters = new Map();
|
|
1425
|
+
const providerChunkTypeCounters = new Map();
|
|
1426
|
+
const contextWriter = args.contextStepStream?.getWriter();
|
|
1427
|
+
const workflowWriter = args.writable?.getWriter();
|
|
1428
|
+
const emitProviderChunk = async (providerChunk) => {
|
|
1429
|
+
const mapped = defaultMapCodexChunk(providerChunk);
|
|
1430
|
+
if (!mapped || mapped.skip)
|
|
1431
|
+
return;
|
|
1432
|
+
sequence += 1;
|
|
1433
|
+
const identity = completeCodexMappedChunkIdentity(mapped, args.stepId);
|
|
1434
|
+
const mappedChunk = {
|
|
1435
|
+
at: new Date().toISOString(),
|
|
1436
|
+
sequence,
|
|
1437
|
+
chunkType: mapped.chunkType,
|
|
1438
|
+
providerChunkType: mapped.providerChunkType,
|
|
1439
|
+
partId: identity.partId,
|
|
1440
|
+
providerPartId: identity.providerPartId,
|
|
1441
|
+
partType: identity.partType,
|
|
1442
|
+
partSlot: identity.partSlot,
|
|
1443
|
+
actionRef: identity.actionRef,
|
|
1444
|
+
data: mapped.data,
|
|
1445
|
+
raw: mapped.raw ?? toJsonSafe(providerChunk),
|
|
1446
|
+
};
|
|
1447
|
+
mappedChunks.push(mappedChunk);
|
|
1448
|
+
chunkTypeCounters.set(mappedChunk.chunkType, (chunkTypeCounters.get(mappedChunk.chunkType) ?? 0) + 1);
|
|
1449
|
+
const providerType = mappedChunk.providerChunkType || "unknown";
|
|
1450
|
+
providerChunkTypeCounters.set(providerType, (providerChunkTypeCounters.get(providerType) ?? 0) + 1);
|
|
1451
|
+
const payload = {
|
|
1452
|
+
at: mappedChunk.at,
|
|
1453
|
+
sequence,
|
|
1454
|
+
chunkType: mappedChunk.chunkType,
|
|
1455
|
+
provider: "codex",
|
|
1456
|
+
providerChunkType: mappedChunk.providerChunkType,
|
|
1457
|
+
partId: mappedChunk.partId,
|
|
1458
|
+
providerPartId: mappedChunk.providerPartId,
|
|
1459
|
+
partType: mappedChunk.partType,
|
|
1460
|
+
partSlot: mappedChunk.partSlot,
|
|
1461
|
+
actionRef: mappedChunk.actionRef,
|
|
1462
|
+
data: mappedChunk.data,
|
|
1463
|
+
raw: mappedChunk.raw,
|
|
1464
|
+
};
|
|
1465
|
+
await contextWriter?.write(encodeContextStepStreamChunk(createContextStepStreamChunk({
|
|
1466
|
+
...payload,
|
|
1467
|
+
stepId: args.stepId,
|
|
1468
|
+
})));
|
|
1469
|
+
await workflowWriter?.write({
|
|
1470
|
+
type: "data-chunk.emitted",
|
|
1471
|
+
data: {
|
|
1472
|
+
type: "chunk.emitted",
|
|
1473
|
+
contextId: args.contextId,
|
|
1474
|
+
executionId: args.executionId,
|
|
1475
|
+
stepId: args.stepId,
|
|
1476
|
+
itemId: args.eventId,
|
|
1477
|
+
...payload,
|
|
1478
|
+
},
|
|
1479
|
+
});
|
|
1480
|
+
};
|
|
1481
|
+
const streamTrace = () => ({
|
|
1482
|
+
totalChunks: mappedChunks.length,
|
|
1483
|
+
chunkTypes: Object.fromEntries(chunkTypeCounters.entries()),
|
|
1484
|
+
providerChunkTypes: Object.fromEntries(providerChunkTypeCounters.entries()),
|
|
1485
|
+
chunks: mappedChunks,
|
|
1486
|
+
});
|
|
1487
|
+
try {
|
|
1488
|
+
if (args.config.mode === "sandbox" || args.config.sandbox) {
|
|
1489
|
+
return await executeCodexSandboxTurn(args, {
|
|
1490
|
+
emitProviderChunk,
|
|
1491
|
+
streamTrace,
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (String(args.config.appServerUrl || "").trim().replace(/\/+$/, "").endsWith("/turn")) {
|
|
1495
|
+
const response = await fetch(args.config.appServerUrl, {
|
|
1496
|
+
method: "POST",
|
|
1497
|
+
headers: { "content-type": "application/json" },
|
|
1498
|
+
body: JSON.stringify({
|
|
1499
|
+
instruction: args.instruction,
|
|
1500
|
+
config: args.config,
|
|
1501
|
+
runtime: { source: "openai-reactor" },
|
|
1502
|
+
}),
|
|
1503
|
+
});
|
|
1504
|
+
const payload = await readJsonResponse(response);
|
|
1505
|
+
if (!response.ok) {
|
|
1506
|
+
throw new Error(asString(payload.error) || `codex_turn_http_${response.status}`);
|
|
1507
|
+
}
|
|
1508
|
+
for (const chunk of asUnknownArray(payload.stream)) {
|
|
1509
|
+
await emitProviderChunk(chunk);
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
providerContextId: asString(payload.providerContextId) ||
|
|
1513
|
+
asString(payload.contextId) ||
|
|
1514
|
+
asString(args.config.providerContextId),
|
|
1515
|
+
turnId: asString(payload.turnId),
|
|
1516
|
+
assistantText: asString(payload.assistantText) || asString(payload.text),
|
|
1517
|
+
reasoningText: asString(payload.reasoningText) || asString(payload.reasoning),
|
|
1518
|
+
diff: asString(payload.diff),
|
|
1519
|
+
toolParts: asUnknownArray(payload.toolParts),
|
|
1520
|
+
usage: asRecord(payload.usage),
|
|
1521
|
+
metadata: {
|
|
1522
|
+
provider: "codex-app-server",
|
|
1523
|
+
response: payload,
|
|
1524
|
+
streamTrace: streamTrace(),
|
|
1525
|
+
},
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
return await executeCodexHttpTurn(args, {
|
|
1529
|
+
emitProviderChunk,
|
|
1530
|
+
streamTrace,
|
|
1531
|
+
}, baseUrl);
|
|
1532
|
+
const eventsResponse = await fetch(`${baseUrl}/events`, {
|
|
1533
|
+
method: "GET",
|
|
1534
|
+
headers: { accept: "text/event-stream" },
|
|
1535
|
+
});
|
|
1536
|
+
if (!eventsResponse.ok || !eventsResponse.body) {
|
|
1537
|
+
throw new Error(`codex_events_unavailable_${eventsResponse.status}`);
|
|
1538
|
+
}
|
|
1539
|
+
const requestedThreadId = asString(args.config.providerContextId).trim();
|
|
1540
|
+
let providerContextId = requestedThreadId;
|
|
1541
|
+
if (providerContextId && isValidProviderContextId(providerContextId)) {
|
|
1542
|
+
await codexAppServerRpc(baseUrl, "thread/resume", { threadId: providerContextId });
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
const startParams = {
|
|
1546
|
+
cwd: args.config.repoPath,
|
|
1547
|
+
approvalPolicy: args.config.approvalPolicy ?? "never",
|
|
1548
|
+
sandboxPolicy: args.config.sandboxPolicy && Object.keys(args.config.sandboxPolicy ?? {}).length > 0
|
|
1549
|
+
? args.config.sandboxPolicy
|
|
1550
|
+
: { type: "externalSandbox", networkAccess: "enabled" },
|
|
1551
|
+
};
|
|
1552
|
+
if (args.config.model)
|
|
1553
|
+
startParams.model = args.config.model;
|
|
1554
|
+
const started = await codexAppServerRpc(baseUrl, "thread/start", startParams);
|
|
1555
|
+
providerContextId =
|
|
1556
|
+
asString(asRecord(asRecord(started.result).thread).id) ||
|
|
1557
|
+
asString(asRecord(started.result).id) ||
|
|
1558
|
+
asString(started.threadId);
|
|
1559
|
+
}
|
|
1560
|
+
if (!providerContextId)
|
|
1561
|
+
throw new Error("codex_thread_id_missing");
|
|
1562
|
+
const turnParams = {
|
|
1563
|
+
threadId: providerContextId,
|
|
1564
|
+
input: [{ type: "text", text: args.instruction }],
|
|
1565
|
+
cwd: args.config.repoPath,
|
|
1566
|
+
approvalPolicy: args.config.approvalPolicy ?? "never",
|
|
1567
|
+
sandboxPolicy: args.config.sandboxPolicy && Object.keys(args.config.sandboxPolicy ?? {}).length > 0
|
|
1568
|
+
? args.config.sandboxPolicy
|
|
1569
|
+
: { type: "externalSandbox", networkAccess: "enabled" },
|
|
1570
|
+
};
|
|
1571
|
+
if (args.config.model)
|
|
1572
|
+
turnParams.model = args.config.model;
|
|
1573
|
+
const turnStart = await codexAppServerRpc(baseUrl, "turn/start", turnParams);
|
|
1574
|
+
let turnId = asString(asRecord(asRecord(turnStart.result).turn).id) ||
|
|
1575
|
+
asString(asRecord(turnStart.result).id) ||
|
|
1576
|
+
asString(turnStart.turnId);
|
|
1577
|
+
const reader = eventsResponse.body.getReader();
|
|
1578
|
+
const decoder = new TextDecoder();
|
|
1579
|
+
let buffer = "";
|
|
1580
|
+
let assistantText = "";
|
|
1581
|
+
let reasoningText = "";
|
|
1582
|
+
let diff = "";
|
|
1583
|
+
let usage = {};
|
|
1584
|
+
let completedTurn = {};
|
|
1585
|
+
try {
|
|
1586
|
+
while (true) {
|
|
1587
|
+
const read = await reader.read();
|
|
1588
|
+
if (read.done)
|
|
1589
|
+
break;
|
|
1590
|
+
if (!read.value)
|
|
1591
|
+
continue;
|
|
1592
|
+
buffer += decoder.decode(read.value, { stream: true });
|
|
1593
|
+
const blocks = buffer.split("\n\n");
|
|
1594
|
+
buffer = blocks.pop() ?? "";
|
|
1595
|
+
for (const block of blocks) {
|
|
1596
|
+
const data = parseSseDataBlock(block);
|
|
1597
|
+
if (!data || data === "[DONE]")
|
|
1598
|
+
continue;
|
|
1599
|
+
const evt = asRecord(JSON.parse(String(data)));
|
|
1600
|
+
const method = asString(evt.method);
|
|
1601
|
+
if (!method)
|
|
1602
|
+
continue;
|
|
1603
|
+
const params = asRecord(evt.params);
|
|
1604
|
+
const evtTurnId = asString(params.turnId) || asString(asRecord(params.turn).id);
|
|
1605
|
+
const evtThreadId = asString(params.threadId) ||
|
|
1606
|
+
asString(params.providerContextId) ||
|
|
1607
|
+
asString(asRecord(params.turn).threadId) ||
|
|
1608
|
+
asString(asRecord(params.turn).providerContextId);
|
|
1609
|
+
const scopedToTurn = (evtTurnId && turnId && evtTurnId === turnId) ||
|
|
1610
|
+
(evtThreadId && evtThreadId === providerContextId) ||
|
|
1611
|
+
method.startsWith("thread/") ||
|
|
1612
|
+
method.startsWith("context/");
|
|
1613
|
+
if (!scopedToTurn)
|
|
1614
|
+
continue;
|
|
1615
|
+
await emitProviderChunk(evt);
|
|
1616
|
+
if (method === "turn/started" && !turnId) {
|
|
1617
|
+
turnId = asString(asRecord(params.turn).id) || evtTurnId;
|
|
1618
|
+
}
|
|
1619
|
+
if (method === "item/agentMessage/delta") {
|
|
1620
|
+
assistantText += asString(params.delta);
|
|
1621
|
+
}
|
|
1622
|
+
if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
|
|
1623
|
+
reasoningText += asString(params.delta);
|
|
1624
|
+
}
|
|
1625
|
+
if (method === "turn/diff/updated") {
|
|
1626
|
+
diff = asString(params.diff);
|
|
1627
|
+
}
|
|
1628
|
+
if (method === "thread/tokenUsage/updated" || method === "context/tokenUsage/updated") {
|
|
1629
|
+
usage = asRecord(params.tokenUsage);
|
|
1630
|
+
}
|
|
1631
|
+
if (method === "item/completed") {
|
|
1632
|
+
const item = asRecord(params.item);
|
|
1633
|
+
if (asString(item.type) === "agentMessage" && asString(item.text).trim()) {
|
|
1634
|
+
assistantText = asString(item.text);
|
|
1635
|
+
}
|
|
1636
|
+
if (asString(item.type) === "reasoning" && asString(item.summary).trim()) {
|
|
1637
|
+
reasoningText = asString(item.summary);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (method === "turn/completed") {
|
|
1641
|
+
completedTurn = asRecord(params.turn);
|
|
1642
|
+
return {
|
|
1643
|
+
providerContextId,
|
|
1644
|
+
turnId: asString(completedTurn.id) || turnId,
|
|
1645
|
+
assistantText,
|
|
1646
|
+
reasoningText,
|
|
1647
|
+
diff,
|
|
1648
|
+
toolParts: asUnknownArray(completedTurn.toolParts),
|
|
1649
|
+
usage,
|
|
1650
|
+
metadata: {
|
|
1651
|
+
provider: "codex-app-server",
|
|
1652
|
+
providerResponse: completedTurn,
|
|
1653
|
+
streamTrace: streamTrace(),
|
|
1654
|
+
},
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
if (method === "turn/failed") {
|
|
1658
|
+
throw new Error(`codex_turn_failed_${evtTurnId || turnId || "unknown"}`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
finally {
|
|
1664
|
+
await reader.cancel().catch(() => { });
|
|
1665
|
+
}
|
|
1666
|
+
throw new Error("codex_turn_completion_missing");
|
|
1667
|
+
}
|
|
1668
|
+
finally {
|
|
1669
|
+
contextWriter?.releaseLock();
|
|
1670
|
+
workflowWriter?.releaseLock();
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
281
1673
|
/**
|
|
282
1674
|
* Codex App Server reactor for @ekairos/events.
|
|
283
1675
|
*
|
|
@@ -298,7 +1690,6 @@ export function createCodexReactor(options) {
|
|
|
298
1690
|
const maxPersistedStreamChunks = Math.max(0, Number(options.maxPersistedStreamChunks ?? 300));
|
|
299
1691
|
return async (params) => {
|
|
300
1692
|
let chunkSequence = 0;
|
|
301
|
-
const contextStepStreamWriter = params.contextStepStream?.getWriter();
|
|
302
1693
|
const chunkTypeCounters = new Map();
|
|
303
1694
|
const providerChunkTypeCounters = new Map();
|
|
304
1695
|
const capturedChunks = [];
|
|
@@ -307,13 +1698,12 @@ export function createCodexReactor(options) {
|
|
|
307
1698
|
const context = asRecord(params.context.content);
|
|
308
1699
|
const instruction = (options.buildInstruction
|
|
309
1700
|
? await options.buildInstruction({
|
|
310
|
-
env: params.env,
|
|
311
1701
|
context,
|
|
312
1702
|
triggerEvent: params.triggerEvent,
|
|
313
1703
|
})
|
|
314
1704
|
: defaultInstructionFromTrigger(params.triggerEvent)).trim();
|
|
315
1705
|
const config = await options.resolveConfig({
|
|
316
|
-
|
|
1706
|
+
runtime: params.runtime,
|
|
317
1707
|
context,
|
|
318
1708
|
triggerEvent: params.triggerEvent,
|
|
319
1709
|
contextId: params.contextId,
|
|
@@ -322,6 +1712,27 @@ export function createCodexReactor(options) {
|
|
|
322
1712
|
stepId: params.stepId,
|
|
323
1713
|
iteration: params.iteration,
|
|
324
1714
|
});
|
|
1715
|
+
const persistedReactor = asRecord(params.context.reactor);
|
|
1716
|
+
const persistedReactorState = asRecord(persistedReactor.state);
|
|
1717
|
+
if (!config.providerContextId) {
|
|
1718
|
+
const providerContextId = asString(persistedReactorState.providerContextId);
|
|
1719
|
+
if (providerContextId)
|
|
1720
|
+
config.providerContextId = providerContextId;
|
|
1721
|
+
}
|
|
1722
|
+
if (config.sandbox) {
|
|
1723
|
+
const sandboxState = asRecord(persistedReactorState.sandbox);
|
|
1724
|
+
if (!config.sandbox.sandboxId) {
|
|
1725
|
+
const sandboxId = asString(sandboxState.sandboxId);
|
|
1726
|
+
if (sandboxId)
|
|
1727
|
+
config.sandbox.sandboxId = sandboxId;
|
|
1728
|
+
}
|
|
1729
|
+
if (!config.repoPath) {
|
|
1730
|
+
const repoPath = asString(sandboxState.repoPath);
|
|
1731
|
+
if (repoPath)
|
|
1732
|
+
config.repoPath = repoPath;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
const effectiveActionSpecs = toCodexActionSpecs(actionsToActionSpecs(params.actions));
|
|
325
1736
|
const startedAtMs = Date.now();
|
|
326
1737
|
let streamedAssistantText = "";
|
|
327
1738
|
let streamedReasoningText = "";
|
|
@@ -333,6 +1744,8 @@ export function createCodexReactor(options) {
|
|
|
333
1744
|
const mappedMethod = asString(mappedData.method);
|
|
334
1745
|
if (mappedMethod !== "item/started" &&
|
|
335
1746
|
mappedMethod !== "item/completed" &&
|
|
1747
|
+
mappedMethod !== "item/tool/call" &&
|
|
1748
|
+
mappedMethod !== "item/tool/result" &&
|
|
336
1749
|
mappedMethod !== "thread/tokenUsage/updated" &&
|
|
337
1750
|
mappedMethod !== "context/tokenUsage/updated" &&
|
|
338
1751
|
mappedMethod !== "turn/completed" &&
|
|
@@ -381,12 +1794,17 @@ export function createCodexReactor(options) {
|
|
|
381
1794
|
return;
|
|
382
1795
|
const now = new Date().toISOString();
|
|
383
1796
|
chunkSequence += 1;
|
|
1797
|
+
const identity = completeCodexMappedChunkIdentity(mapped, params.stepId);
|
|
384
1798
|
const mappedChunk = {
|
|
385
1799
|
at: now,
|
|
386
1800
|
sequence: chunkSequence,
|
|
387
1801
|
chunkType: mapped.chunkType,
|
|
388
1802
|
providerChunkType: mapped.providerChunkType,
|
|
389
|
-
|
|
1803
|
+
partId: identity.partId,
|
|
1804
|
+
providerPartId: identity.providerPartId,
|
|
1805
|
+
partType: identity.partType,
|
|
1806
|
+
partSlot: identity.partSlot,
|
|
1807
|
+
actionRef: identity.actionRef,
|
|
390
1808
|
data: mapped.data,
|
|
391
1809
|
raw: includeRawProviderChunksInOutput
|
|
392
1810
|
? mapped.raw ?? toJsonSafe(providerChunk)
|
|
@@ -448,12 +1866,25 @@ export function createCodexReactor(options) {
|
|
|
448
1866
|
chunkType: mappedChunk.chunkType,
|
|
449
1867
|
provider: "codex",
|
|
450
1868
|
providerChunkType: mappedChunk.providerChunkType,
|
|
1869
|
+
partId: mappedChunk.partId,
|
|
1870
|
+
providerPartId: mappedChunk.providerPartId,
|
|
1871
|
+
partType: mappedChunk.partType,
|
|
1872
|
+
partSlot: mappedChunk.partSlot,
|
|
451
1873
|
actionRef: mappedChunk.actionRef,
|
|
452
1874
|
data: mappedChunk.data,
|
|
453
1875
|
raw: mapped.raw ?? toJsonSafe(providerChunk),
|
|
454
1876
|
};
|
|
455
|
-
if (
|
|
456
|
-
|
|
1877
|
+
if (params.contextStepStream) {
|
|
1878
|
+
const writer = params.contextStepStream.getWriter();
|
|
1879
|
+
try {
|
|
1880
|
+
await writer.write(encodeContextStepStreamChunk(createContextStepStreamChunk({
|
|
1881
|
+
...payload,
|
|
1882
|
+
stepId: params.stepId,
|
|
1883
|
+
})));
|
|
1884
|
+
}
|
|
1885
|
+
finally {
|
|
1886
|
+
writer.releaseLock();
|
|
1887
|
+
}
|
|
457
1888
|
}
|
|
458
1889
|
if (params.writable) {
|
|
459
1890
|
const writer = params.writable.getWriter();
|
|
@@ -475,9 +1906,9 @@ export function createCodexReactor(options) {
|
|
|
475
1906
|
}
|
|
476
1907
|
}
|
|
477
1908
|
};
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1909
|
+
const turn = options.executeTurn
|
|
1910
|
+
? await options.executeTurn({
|
|
1911
|
+
runtime: params.runtime,
|
|
481
1912
|
context,
|
|
482
1913
|
triggerEvent: params.triggerEvent,
|
|
483
1914
|
contextId: params.contextId,
|
|
@@ -487,71 +1918,113 @@ export function createCodexReactor(options) {
|
|
|
487
1918
|
iteration: params.iteration,
|
|
488
1919
|
instruction,
|
|
489
1920
|
config,
|
|
1921
|
+
actions: params.actions,
|
|
1922
|
+
actionSpecs: effectiveActionSpecs,
|
|
490
1923
|
skills: params.skills,
|
|
1924
|
+
storedContext: params.context,
|
|
1925
|
+
contextIdentifier: params.contextIdentifier,
|
|
1926
|
+
contextStepStream: params.contextStepStream,
|
|
491
1927
|
writable: params.writable,
|
|
492
|
-
silent: params.silent,
|
|
493
1928
|
emitChunk,
|
|
1929
|
+
})
|
|
1930
|
+
: await executeCodexAppServerTurnStep({
|
|
1931
|
+
config,
|
|
1932
|
+
runtime: params.runtime,
|
|
1933
|
+
instruction,
|
|
1934
|
+
systemPrompt: params.systemPrompt,
|
|
1935
|
+
contextId: params.contextId,
|
|
1936
|
+
eventId: params.eventId,
|
|
1937
|
+
executionId: params.executionId,
|
|
1938
|
+
stepId: params.stepId,
|
|
1939
|
+
iteration: params.iteration,
|
|
1940
|
+
context,
|
|
1941
|
+
actions: params.actions,
|
|
1942
|
+
actionSpecs: effectiveActionSpecs,
|
|
1943
|
+
storedContext: params.context,
|
|
1944
|
+
contextIdentifier: params.contextIdentifier,
|
|
1945
|
+
contextStepStream: params.contextStepStream,
|
|
1946
|
+
writable: params.writable,
|
|
494
1947
|
});
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
1948
|
+
const finishedAtMs = Date.now();
|
|
1949
|
+
const returnedStreamTrace = asRecord(asRecord(turn.metadata).streamTrace);
|
|
1950
|
+
const returnedChunks = Array.isArray(returnedStreamTrace.chunks)
|
|
1951
|
+
? returnedStreamTrace.chunks
|
|
1952
|
+
: [];
|
|
1953
|
+
const effectiveRawChunks = allCapturedChunks.length > 0 ? allCapturedChunks : returnedChunks;
|
|
1954
|
+
const effectiveSemanticChunks = semanticChunks.length > 0 ? semanticChunks : returnedChunks;
|
|
1955
|
+
const returnedChunkTypes = asNumberRecord(returnedStreamTrace.chunkTypes);
|
|
1956
|
+
const returnedProviderChunkTypes = asNumberRecord(returnedStreamTrace.providerChunkTypes);
|
|
1957
|
+
const returnedTotalChunks = typeof returnedStreamTrace.totalChunks === "number"
|
|
1958
|
+
? returnedStreamTrace.totalChunks
|
|
1959
|
+
: returnedChunks.length;
|
|
1960
|
+
const streamTrace = includeStreamTraceInOutput
|
|
1961
|
+
? {
|
|
1962
|
+
totalChunks: chunkSequence || returnedTotalChunks,
|
|
1963
|
+
chunkTypes: chunkSequence > 0
|
|
1964
|
+
? Object.fromEntries(chunkTypeCounters.entries())
|
|
1965
|
+
: returnedChunkTypes,
|
|
1966
|
+
providerChunkTypes: chunkSequence > 0
|
|
1967
|
+
? Object.fromEntries(providerChunkTypeCounters.entries())
|
|
1968
|
+
: returnedProviderChunkTypes,
|
|
1969
|
+
}
|
|
1970
|
+
: undefined;
|
|
1971
|
+
const usagePayload = toJsonSafe(turn.usage ?? asRecord(turn.metadata).usage);
|
|
1972
|
+
const usageMetrics = extractUsageMetrics(usagePayload);
|
|
1973
|
+
const assistantEvent = {
|
|
1974
|
+
id: params.eventId,
|
|
1975
|
+
type: OUTPUT_ITEM_TYPE,
|
|
1976
|
+
channel: "web",
|
|
1977
|
+
createdAt: new Date().toISOString(),
|
|
1978
|
+
status: "completed",
|
|
1979
|
+
content: {
|
|
1980
|
+
parts: buildCodexParts({
|
|
1981
|
+
toolName,
|
|
1982
|
+
includeReasoningPart,
|
|
1983
|
+
semanticChunks: effectiveSemanticChunks,
|
|
1984
|
+
rawChunks: effectiveRawChunks,
|
|
1985
|
+
result: turn,
|
|
1986
|
+
instruction,
|
|
1987
|
+
streamTrace,
|
|
1988
|
+
}),
|
|
1989
|
+
},
|
|
1990
|
+
};
|
|
1991
|
+
return {
|
|
1992
|
+
assistantEvent,
|
|
1993
|
+
actionRequests: [],
|
|
1994
|
+
messagesForModel: [],
|
|
1995
|
+
reactor: {
|
|
1996
|
+
kind: "codex",
|
|
1997
|
+
state: {
|
|
1998
|
+
providerContextId: turn.providerContextId,
|
|
1999
|
+
lastTurnId: turn.turnId,
|
|
2000
|
+
provider: asString(asRecord(turn.metadata).provider || "codex"),
|
|
2001
|
+
sandbox: asRecord(asRecord(turn.metadata).sandbox),
|
|
549
2002
|
},
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
2003
|
+
},
|
|
2004
|
+
llm: {
|
|
2005
|
+
provider: "codex",
|
|
2006
|
+
model: asString(config.model || "codex"),
|
|
2007
|
+
promptTokens: usageMetrics.promptTokens,
|
|
2008
|
+
promptTokensCached: usageMetrics.promptTokensCached,
|
|
2009
|
+
promptTokensUncached: usageMetrics.promptTokensUncached,
|
|
2010
|
+
completionTokens: usageMetrics.completionTokens,
|
|
2011
|
+
totalTokens: usageMetrics.totalTokens,
|
|
2012
|
+
latencyMs: Math.max(0, finishedAtMs - startedAtMs),
|
|
2013
|
+
rawUsage: usagePayload,
|
|
2014
|
+
rawProviderMetadata: toJsonSafe({
|
|
2015
|
+
providerContextId: turn.providerContextId,
|
|
2016
|
+
turnId: turn.turnId,
|
|
2017
|
+
metadata: turn.metadata ?? null,
|
|
2018
|
+
streamTrace: streamTrace
|
|
2019
|
+
? {
|
|
2020
|
+
totalChunks: streamTrace.totalChunks,
|
|
2021
|
+
chunkTypes: streamTrace.chunkTypes,
|
|
2022
|
+
providerChunkTypes: streamTrace.providerChunkTypes,
|
|
2023
|
+
}
|
|
2024
|
+
: undefined,
|
|
2025
|
+
}),
|
|
2026
|
+
},
|
|
2027
|
+
};
|
|
555
2028
|
};
|
|
556
2029
|
}
|
|
557
2030
|
//# sourceMappingURL=codex.reactor.js.map
|