@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.
@@ -1,5 +1,28 @@
1
- import { OUTPUT_ITEM_TYPE, createContextStepStreamChunk, encodeContextStepStreamChunk, } from "@ekairos/events";
2
- import { asRecord, asString, buildCodexParts, defaultInstructionFromTrigger } from "./shared.js";
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("action_input_start") || value.includes("tool_input_start")) {
34
- return "chunk.action_input_start";
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.action_input_available";
73
+ return "chunk.action_started";
43
74
  }
44
75
  if (value.includes("action_output_available") || value.includes("tool_output_available")) {
45
- return "chunk.action_output_available";
76
+ return "chunk.action_completed";
46
77
  }
47
78
  if (value.includes("action_output_error") || value.includes("tool_output_error")) {
48
- return "chunk.action_output_error";
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 mappedData = toJsonSafe({
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
- const map = (chunkType) => ({
129
- chunkType,
130
- providerChunkType: method,
131
- actionRef: chunkType.startsWith("chunk.action_") ? actionRef : undefined,
132
- data: mappedData,
133
- raw: toJsonSafe(providerChunk),
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.action_output_available");
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.action_input_available");
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.action_output_error");
331
+ return map("chunk.action_failed");
193
332
  }
194
- return map("chunk.action_output_available");
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: mapCodexChunkType(providerChunkType),
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
- env: params.env,
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
- actionRef: mapped.actionRef,
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 (contextStepStreamWriter) {
456
- await contextStepStreamWriter.write(encodeContextStepStreamChunk(createContextStepStreamChunk(payload)));
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
- try {
479
- const turn = await options.executeTurn({
480
- env: params.env,
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
- const finishedAtMs = Date.now();
496
- const streamTrace = includeStreamTraceInOutput
497
- ? {
498
- totalChunks: chunkSequence,
499
- chunkTypes: Object.fromEntries(chunkTypeCounters.entries()),
500
- providerChunkTypes: Object.fromEntries(providerChunkTypeCounters.entries()),
501
- }
502
- : undefined;
503
- const usagePayload = toJsonSafe(turn.usage ?? asRecord(turn.metadata).usage);
504
- const usageMetrics = extractUsageMetrics(usagePayload);
505
- const assistantEvent = {
506
- id: params.eventId,
507
- type: OUTPUT_ITEM_TYPE,
508
- channel: "web",
509
- createdAt: new Date().toISOString(),
510
- status: "completed",
511
- content: {
512
- parts: buildCodexParts({
513
- toolName,
514
- includeReasoningPart,
515
- semanticChunks,
516
- rawChunks: allCapturedChunks,
517
- result: turn,
518
- instruction,
519
- streamTrace,
520
- }),
521
- },
522
- };
523
- return {
524
- assistantEvent,
525
- actionRequests: [],
526
- messagesForModel: [],
527
- llm: {
528
- provider: "codex",
529
- model: asString(config.model || "codex"),
530
- promptTokens: usageMetrics.promptTokens,
531
- promptTokensCached: usageMetrics.promptTokensCached,
532
- promptTokensUncached: usageMetrics.promptTokensUncached,
533
- completionTokens: usageMetrics.completionTokens,
534
- totalTokens: usageMetrics.totalTokens,
535
- latencyMs: Math.max(0, finishedAtMs - startedAtMs),
536
- rawUsage: usagePayload,
537
- rawProviderMetadata: toJsonSafe({
538
- providerContextId: turn.providerContextId,
539
- turnId: turn.turnId,
540
- metadata: turn.metadata ?? null,
541
- streamTrace: streamTrace
542
- ? {
543
- totalChunks: streamTrace.totalChunks,
544
- chunkTypes: streamTrace.chunkTypes,
545
- providerChunkTypes: streamTrace.providerChunkTypes,
546
- }
547
- : undefined,
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
- finally {
553
- contextStepStreamWriter?.releaseLock();
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