@ekairos/openai-reactor 1.22.34-beta.development.0 → 1.22.35-beta.development.0

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,4 +1,5 @@
1
1
  import { OUTPUT_ITEM_TYPE, createContextStepStreamChunk, encodeContextStepStreamChunk, } from "@ekairos/events";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { asRecord, asString, buildCodexParts, defaultInstructionFromTrigger } from "./shared.js";
3
4
  const PROVIDER_SCOPE_PREFIX = "context/";
4
5
  const PROVIDER_STARTED = "context/started";
@@ -89,6 +90,7 @@ function isActionItemType(itemType) {
89
90
  function resolveActionRef(params, item) {
90
91
  const fromParams = asString(params.itemId) ||
91
92
  asString(params.toolCallId) ||
93
+ asString(params.callId) ||
92
94
  asString(params.id);
93
95
  if (fromParams)
94
96
  return fromParams;
@@ -169,6 +171,13 @@ export function mapCodexAppServerNotification(providerChunk) {
169
171
  case "item/fileChange/outputDelta":
170
172
  case "item/mcpToolCall/progress":
171
173
  return map("chunk.action_output_available");
174
+ case "item/tool/call":
175
+ return map("chunk.action_input_available");
176
+ case "item/tool/result":
177
+ if (asRecord(params.result).success === false || asString(params.error)) {
178
+ return map("chunk.action_output_error");
179
+ }
180
+ return map("chunk.action_output_available");
172
181
  case "item/started": {
173
182
  if (itemType === "agentmessage")
174
183
  return map("chunk.text_start");
@@ -278,6 +287,1219 @@ function extractUsageMetrics(usageSource) {
278
287
  promptTokensUncached,
279
288
  };
280
289
  }
290
+ function asUnknownArray(value) {
291
+ return Array.isArray(value) ? value : [];
292
+ }
293
+ function asNumberRecord(value) {
294
+ const record = asRecord(value);
295
+ const out = {};
296
+ for (const [key, entry] of Object.entries(record)) {
297
+ if (typeof entry === "number" && Number.isFinite(entry)) {
298
+ out[key] = entry;
299
+ }
300
+ }
301
+ return out;
302
+ }
303
+ function isValidProviderContextId(value) {
304
+ const normalized = value.trim();
305
+ if (!normalized)
306
+ return false;
307
+ if (/^[0-9a-fA-F-]{36}$/.test(normalized))
308
+ return true;
309
+ if (/^urn:uuid:[0-9a-fA-F-]{36}$/.test(normalized))
310
+ return true;
311
+ return false;
312
+ }
313
+ function normalizeAppServerBaseUrl(raw) {
314
+ const trimmed = String(raw || "").trim().replace(/\/+$/, "");
315
+ if (trimmed.endsWith("/turn"))
316
+ return trimmed.slice(0, -"/turn".length);
317
+ if (trimmed.endsWith("/rpc"))
318
+ return trimmed.slice(0, -"/rpc".length);
319
+ if (trimmed.endsWith("/events"))
320
+ return trimmed.slice(0, -"/events".length);
321
+ return trimmed;
322
+ }
323
+ function parseSseDataBlock(block) {
324
+ const lines = block.split("\n").map((line) => line.trimEnd());
325
+ const dataLines = lines.filter((line) => line.startsWith("data:"));
326
+ if (!dataLines.length)
327
+ return null;
328
+ return dataLines.map((line) => line.replace(/^data:\s*/, "")).join("\n");
329
+ }
330
+ function shellSingleQuote(value) {
331
+ return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
332
+ }
333
+ function stripProviderControlChars(value) {
334
+ return String(value ?? "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, "");
335
+ }
336
+ function parseSandboxJsonl(stdout) {
337
+ const events = [];
338
+ let result = {};
339
+ for (const rawLine of stripProviderControlChars(stdout).split(/\r?\n/)) {
340
+ const line = rawLine.trim();
341
+ if (!line.startsWith("EKAIROS_CODEX_"))
342
+ continue;
343
+ const tab = line.indexOf("\t");
344
+ if (tab === -1)
345
+ continue;
346
+ const prefix = line.slice(0, tab);
347
+ const payload = asRecord(JSON.parse(line.slice(tab + 1)));
348
+ if (prefix === "EKAIROS_CODEX_EVENT")
349
+ events.push(payload);
350
+ if (prefix === "EKAIROS_CODEX_RESULT")
351
+ result = payload;
352
+ }
353
+ return { events, result };
354
+ }
355
+ function buildCodexDynamicTools(actionSpecs) {
356
+ const specs = actionSpecs && typeof actionSpecs === "object" ? actionSpecs : {};
357
+ return Object.entries(specs)
358
+ .map(([name, spec]) => {
359
+ const toolName = asString(name).trim();
360
+ if (!toolName)
361
+ return null;
362
+ return {
363
+ name: toolName,
364
+ description: asString(spec?.description).trim() || `Run ${toolName}.`,
365
+ inputSchema: spec && "inputSchema" in spec && spec.inputSchema !== undefined
366
+ ? spec.inputSchema
367
+ : { type: "object", additionalProperties: true },
368
+ };
369
+ })
370
+ .filter(Boolean);
371
+ }
372
+ function toCodexActionSpecs(value) {
373
+ const specs = asRecord(value);
374
+ const out = {};
375
+ for (const [name, spec] of Object.entries(specs)) {
376
+ const record = asRecord(spec);
377
+ if (!record)
378
+ continue;
379
+ if (record.type === "provider-defined")
380
+ continue;
381
+ out[name] = {
382
+ type: record.type === "function" ? "function" : undefined,
383
+ description: asString(record.description) || undefined,
384
+ inputSchema: record.inputSchema,
385
+ };
386
+ }
387
+ return out;
388
+ }
389
+ function formatCodexToolOutput(value) {
390
+ if (typeof value === "string")
391
+ return value;
392
+ try {
393
+ return JSON.stringify(toJsonSafe(value) ?? value);
394
+ }
395
+ catch {
396
+ return String(value);
397
+ }
398
+ }
399
+ async function executeCodexDynamicToolCall(args, params) {
400
+ const toolName = asString(params.tool).trim();
401
+ const callId = asString(params.callId).trim();
402
+ const action = toolName ? (args.actions ?? {})[toolName] : undefined;
403
+ const input = "arguments" in params ? params.arguments : {};
404
+ if (!toolName || !action || typeof action.execute !== "function") {
405
+ const errorText = `codex_dynamic_tool_not_found:${toolName || "unknown"}`;
406
+ return {
407
+ success: false,
408
+ output: { error: errorText },
409
+ errorText,
410
+ response: {
411
+ success: false,
412
+ contentItems: [{ type: "inputText", text: errorText }],
413
+ },
414
+ };
415
+ }
416
+ try {
417
+ const output = await action.execute(input, {
418
+ runtime: args.runtime,
419
+ env: args.env,
420
+ context: args.storedContext ?? args.context,
421
+ contextIdentifier: args.contextIdentifier,
422
+ toolCallId: callId || undefined,
423
+ messages: [],
424
+ eventId: args.eventId,
425
+ executionId: args.executionId,
426
+ triggerEventId: undefined,
427
+ contextId: args.contextId,
428
+ stepId: args.stepId,
429
+ iteration: args.iteration ?? 0,
430
+ });
431
+ return {
432
+ success: true,
433
+ output,
434
+ response: {
435
+ success: true,
436
+ contentItems: [{ type: "inputText", text: formatCodexToolOutput(output) }],
437
+ },
438
+ };
439
+ }
440
+ catch (error) {
441
+ const errorText = error instanceof Error ? error.message : String(error);
442
+ return {
443
+ success: false,
444
+ output: { error: errorText },
445
+ errorText,
446
+ response: {
447
+ success: false,
448
+ contentItems: [{ type: "inputText", text: `Action failed: ${errorText}` }],
449
+ },
450
+ };
451
+ }
452
+ }
453
+ async function codexAppServerRespond(baseUrl, payload) {
454
+ const response = await fetch(`${baseUrl}/respond`, {
455
+ method: "POST",
456
+ headers: { "content-type": "application/json" },
457
+ body: JSON.stringify(payload),
458
+ });
459
+ const body = await readJsonResponse(response);
460
+ if (!response.ok || body.error) {
461
+ throw new Error(asString(body.error) || `codex_respond_http_${response.status}`);
462
+ }
463
+ }
464
+ function codexSandboxBridgeScript() {
465
+ return String.raw `
466
+ import http from "node:http";
467
+ import { spawn } from "node:child_process";
468
+ import { createInterface } from "node:readline";
469
+ import { randomUUID } from "node:crypto";
470
+
471
+ const PORT = Number(process.env.CODEX_BRIDGE_PORT || "4500");
472
+ function asRecord(value) { return value && typeof value === "object" ? value : {}; }
473
+ function asString(value) { return typeof value === "string" ? value : value == null ? "" : String(value); }
474
+ const child = spawn("codex", ["app-server", "--enable", "apps"], { stdio: ["pipe", "pipe", "inherit"], env: process.env });
475
+ const rl = createInterface({ input: child.stdout });
476
+ const pending = new Map();
477
+ const subscribers = new Set();
478
+ let initialized = false;
479
+
480
+ function notifyAll(payload) {
481
+ const data = "data: " + JSON.stringify(payload) + "\n\n";
482
+ for (const res of subscribers) {
483
+ try { res.write(data); } catch { subscribers.delete(res); }
484
+ }
485
+ }
486
+ rl.on("line", (line) => {
487
+ let msg;
488
+ try { msg = JSON.parse(line); } catch { return; }
489
+ if (msg && msg.id && pending.has(msg.id)) {
490
+ const p = pending.get(msg.id);
491
+ pending.delete(msg.id);
492
+ clearTimeout(p.timer);
493
+ if (msg.error) {
494
+ const err = asRecord(msg.error);
495
+ p.reject(new Error(asString(err.message) || asString(msg.error) || "rpc_error"));
496
+ } else {
497
+ p.resolve(msg);
498
+ }
499
+ return;
500
+ }
501
+ notifyAll(msg);
502
+ });
503
+ function sendRpc(payload, timeoutMs = 60000) {
504
+ const id = payload.id || randomUUID();
505
+ const msg = { ...payload, id };
506
+ return new Promise((resolve, reject) => {
507
+ const timer = setTimeout(() => {
508
+ pending.delete(id);
509
+ reject(new Error("rpc_timeout:" + asString(payload.method)));
510
+ }, timeoutMs);
511
+ pending.set(id, { resolve, reject, timer });
512
+ child.stdin.write(JSON.stringify(msg) + "\n");
513
+ });
514
+ }
515
+ async function ensureInitialized() {
516
+ if (initialized) return;
517
+ await sendRpc({ method: "initialize", params: { clientInfo: { name: "ekairos-sandbox", version: "1.0.0" }, capabilities: { experimentalApi: true } } });
518
+ child.stdin.write(JSON.stringify({ method: "initialized", params: {} }) + "\n");
519
+ initialized = true;
520
+ }
521
+ const server = http.createServer((req, res) => {
522
+ if (req.method === "GET" && req.url === "/health") {
523
+ res.writeHead(200, { "content-type": "application/json" });
524
+ res.end(JSON.stringify({ ok: true, initialized }));
525
+ return;
526
+ }
527
+ if (req.method === "GET" && req.url === "/events") {
528
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
529
+ res.write("data: " + JSON.stringify({ type: "ready" }) + "\n\n");
530
+ subscribers.add(res);
531
+ req.on("close", () => subscribers.delete(res));
532
+ return;
533
+ }
534
+ if (req.method === "POST" && req.url === "/rpc") {
535
+ let body = "";
536
+ req.on("data", (chunk) => { body += chunk; });
537
+ req.on("end", async () => {
538
+ try {
539
+ await ensureInitialized();
540
+ const payload = body ? JSON.parse(body) : {};
541
+ const out = await sendRpc(payload);
542
+ res.writeHead(200, { "content-type": "application/json" });
543
+ res.end(JSON.stringify(out));
544
+ } catch (error) {
545
+ res.writeHead(503, { "content-type": "application/json" });
546
+ res.end(JSON.stringify({ error: asString(error?.message || error) }));
547
+ }
548
+ });
549
+ return;
550
+ }
551
+ if (req.method === "POST" && req.url === "/respond") {
552
+ let body = "";
553
+ req.on("data", (chunk) => { body += chunk; });
554
+ req.on("end", async () => {
555
+ try {
556
+ const payload = body ? JSON.parse(body) : {};
557
+ if (!payload || payload.id === undefined || payload.id === null) {
558
+ res.writeHead(400, { "content-type": "application/json" });
559
+ res.end(JSON.stringify({ error: "response_id_required" }));
560
+ return;
561
+ }
562
+ child.stdin.write(JSON.stringify(payload) + "\n");
563
+ res.writeHead(200, { "content-type": "application/json" });
564
+ res.end(JSON.stringify({ ok: true }));
565
+ } catch (error) {
566
+ res.writeHead(503, { "content-type": "application/json" });
567
+ res.end(JSON.stringify({ error: asString(error?.message || error) }));
568
+ }
569
+ });
570
+ return;
571
+ }
572
+ res.writeHead(404, { "content-type": "application/json" });
573
+ res.end(JSON.stringify({ error: "not_found" }));
574
+ });
575
+ child.on("exit", () => process.exit(1));
576
+ server.listen(PORT, "0.0.0.0", async () => {
577
+ try { await ensureInitialized(); } catch {}
578
+ console.log("[codex-bridge] listening http://0.0.0.0:" + PORT);
579
+ });
580
+ `;
581
+ }
582
+ function codexSandboxTurnRunnerScript() {
583
+ return String.raw `
584
+ import { readFileSync } from "node:fs";
585
+ const baseUrl = (process.env.CODEX_BRIDGE_URL || "http://127.0.0.1:4500").replace(/\/+$/, "");
586
+ const instruction = process.env.CODEX_INSTRUCTION_FILE
587
+ ? readFileSync(process.env.CODEX_INSTRUCTION_FILE, "utf8")
588
+ : process.env.CODEX_INSTRUCTION || "";
589
+ const repoPath = process.env.CODEX_REPO_PATH || process.cwd();
590
+ const providerContextId = process.env.CODEX_PROVIDER_CONTEXT_ID || "";
591
+ const model = process.env.CODEX_MODEL || "";
592
+ function asRecord(value) { return value && typeof value === "object" ? value : {}; }
593
+ function asString(value) { return typeof value === "string" ? value : value == null ? "" : String(value); }
594
+ function emit(prefix, payload) { process.stdout.write(prefix + "\t" + JSON.stringify(payload) + "\n"); }
595
+ async function rpc(method, params) {
596
+ const res = await fetch(baseUrl + "/rpc", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ method, params }) });
597
+ const json = await res.json().catch(() => ({}));
598
+ if (!res.ok || json.error) throw new Error(asString(json.error) || "rpc_failed:" + method);
599
+ return json;
600
+ }
601
+ function parseSse(block) {
602
+ const lines = block.split("\n").map((line) => line.trimEnd()).filter((line) => line.startsWith("data:"));
603
+ if (!lines.length) return null;
604
+ return lines.map((line) => line.replace(/^data:\s*/, "")).join("\n");
605
+ }
606
+ let threadId = providerContextId;
607
+ let turnId = "";
608
+ let assistantText = "";
609
+ let reasoningText = "";
610
+ let diff = "";
611
+ let usage = {};
612
+ let completedTurn = {};
613
+ const eventsResponse = await fetch(baseUrl + "/events", { headers: { accept: "text/event-stream" } });
614
+ if (!eventsResponse.ok || !eventsResponse.body) throw new Error("events_unavailable:" + eventsResponse.status);
615
+ if (threadId) {
616
+ await rpc("thread/resume", { threadId });
617
+ } else {
618
+ const params = { cwd: repoPath, approvalPolicy: "never", sandboxPolicy: { type: "externalSandbox", networkAccess: "enabled" } };
619
+ if (model) params.model = model;
620
+ const started = await rpc("thread/start", params);
621
+ threadId = asString(asRecord(asRecord(started.result).thread).id) || asString(asRecord(started.result).id) || asString(started.threadId);
622
+ }
623
+ if (!threadId) throw new Error("thread_id_missing");
624
+ const reader = eventsResponse.body.getReader();
625
+ const decoder = new TextDecoder();
626
+ let buffer = "";
627
+ const turnParams = { threadId, input: [{ type: "text", text: instruction }], cwd: repoPath, approvalPolicy: "never", sandboxPolicy: { type: "externalSandbox", networkAccess: "enabled" } };
628
+ if (model) turnParams.model = model;
629
+ const turnStart = await rpc("turn/start", turnParams);
630
+ turnId = asString(asRecord(asRecord(turnStart.result).turn).id) || asString(asRecord(turnStart.result).id) || asString(turnStart.turnId);
631
+ let done = false;
632
+ while (!done) {
633
+ const read = await reader.read();
634
+ if (read.done) break;
635
+ buffer += decoder.decode(read.value, { stream: true });
636
+ const blocks = buffer.split("\n\n");
637
+ buffer = blocks.pop() || "";
638
+ for (const block of blocks) {
639
+ const data = parseSse(block);
640
+ if (!data || data === "[DONE]") continue;
641
+ const evt = JSON.parse(data);
642
+ const method = asString(evt.method);
643
+ if (!method || method.startsWith("codex/event/")) continue;
644
+ const params = asRecord(evt.params);
645
+ const evtTurnId = asString(params.turnId) || asString(asRecord(params.turn).id);
646
+ const evtThreadId = asString(params.threadId) || asString(asRecord(params.turn).threadId);
647
+ const scoped = (evtTurnId && turnId && evtTurnId === turnId) || (evtThreadId && evtThreadId === threadId) || method.startsWith("thread/") || method.startsWith("context/");
648
+ if (!scoped) continue;
649
+ emit("EKAIROS_CODEX_EVENT", evt);
650
+ if (method === "turn/started" && !turnId) turnId = evtTurnId || asString(asRecord(params.turn).id);
651
+ if (method === "item/agentMessage/delta") assistantText += asString(params.delta);
652
+ if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") reasoningText += asString(params.delta);
653
+ if (method === "turn/diff/updated") diff = asString(params.diff);
654
+ if (method === "thread/tokenUsage/updated" || method === "context/tokenUsage/updated") usage = asRecord(params.tokenUsage);
655
+ if (method === "item/completed") {
656
+ const item = asRecord(params.item);
657
+ if (asString(item.type) === "agentMessage" && asString(item.text).trim()) assistantText = asString(item.text);
658
+ if (asString(item.type) === "reasoning" && asString(item.summary).trim()) reasoningText = asString(item.summary);
659
+ }
660
+ if (method === "turn/completed") {
661
+ completedTurn = asRecord(params.turn);
662
+ done = true;
663
+ break;
664
+ }
665
+ if (method === "turn/failed") throw new Error("turn_failed:" + (evtTurnId || turnId || "unknown"));
666
+ }
667
+ }
668
+ await reader.cancel().catch(() => {});
669
+ emit("EKAIROS_CODEX_RESULT", { providerContextId: threadId, turnId: asString(completedTurn.id) || turnId, assistantText, reasoningText, diff, usage, completedTurn });
670
+ `;
671
+ }
672
+ function ensureOk(result, label) {
673
+ if (!result.ok)
674
+ throw new Error(`${label}: ${result.error}`);
675
+ return result.data;
676
+ }
677
+ async function executeCodexSandboxTurn(args, helpers) {
678
+ const sandboxConfig = args.config.sandbox ?? {};
679
+ const runtime = (args.runtime || args.env?.runtime);
680
+ if (!runtime || typeof runtime.use !== "function") {
681
+ throw new Error("codex_sandbox_runtime_required");
682
+ }
683
+ const { sandboxDomain } = await import("@ekairos/sandbox");
684
+ const scoped = await runtime.use(sandboxDomain);
685
+ const actions = scoped.actions;
686
+ const sandboxDb = scoped.db;
687
+ if (!actions)
688
+ throw new Error("codex_sandbox_actions_required");
689
+ const provider = sandboxConfig.provider ?? "sprites";
690
+ const homeDir = provider === "vercel" ? "/vercel/sandbox" : "/home/sprite";
691
+ const codexHome = String(sandboxConfig.codexHome ?? `${homeDir}/.codex`).trim() || `${homeDir}/.codex`;
692
+ const bridgePort = Math.max(1, Number(sandboxConfig.bridgePort ?? 4500));
693
+ const appPort = Math.max(1, Number(sandboxConfig.appPort ?? (provider === "vercel" ? 3000 : 8080)));
694
+ const defaultWorkspaceRoot = provider === "vercel" ? "/vercel/sandbox" : "/workspace";
695
+ const repoPath = String(args.config.repoPath || `${defaultWorkspaceRoot}/ekairos-app`).trim() ||
696
+ `${defaultWorkspaceRoot}/ekairos-app`;
697
+ const workRoot = `${defaultWorkspaceRoot}/.ekairos/codex`;
698
+ const bridgePath = `${workRoot}/codex-bridge.mjs`;
699
+ const turnRunnerPath = `${workRoot}/codex-turn-runner.mjs`;
700
+ const instructionPath = `${workRoot}/instruction-${args.executionId}-${args.stepId}.txt`;
701
+ const checkpoints = [];
702
+ const observedCommandProcesses = new Map();
703
+ let sandboxId = String(sandboxConfig.sandboxId ?? "").trim();
704
+ if (!sandboxId) {
705
+ const created = ensureOk(await actions.createSandbox({
706
+ provider: sandboxConfig.provider ?? "sprites",
707
+ runtime: sandboxConfig.runtime ?? "node22",
708
+ purpose: sandboxConfig.purpose ?? "codex-reactor-sandbox",
709
+ ports: Array.from(new Set([bridgePort, appPort, ...(Array.isArray(sandboxConfig.ports) ? sandboxConfig.ports : [])])),
710
+ ...(provider === "sprites"
711
+ ? {
712
+ sprites: {
713
+ name: sandboxConfig.spriteName,
714
+ waitForCapacity: true,
715
+ urlSettings: { auth: "public" },
716
+ deleteOnStop: true,
717
+ },
718
+ }
719
+ : {}),
720
+ ...(provider === "vercel" ? { vercel: sandboxConfig.vercel ?? {} } : {}),
721
+ }), "codex_sandbox_create");
722
+ sandboxId = String(created.sandboxId);
723
+ }
724
+ if (!sandboxId)
725
+ throw new Error("codex_sandbox_id_missing");
726
+ const emitAndObserveProviderChunk = async (providerChunk) => {
727
+ await helpers.emitProviderChunk(providerChunk);
728
+ const evt = asRecord(providerChunk);
729
+ const method = asString(evt.method);
730
+ const params = asRecord(evt.params);
731
+ if (!method)
732
+ return;
733
+ if (method === "item/started") {
734
+ const item = asRecord(params.item);
735
+ if (asString(item.type) !== "commandExecution")
736
+ return;
737
+ const codexItemId = asString(item.id);
738
+ if (!codexItemId || observedCommandProcesses.has(codexItemId))
739
+ return;
740
+ if (!sandboxDb?.streams?.createWriteStream || !sandboxDb?.tx?.sandbox_processes)
741
+ return;
742
+ const processId = randomUUID();
743
+ const streamClientId = `sandbox-process:${processId}`;
744
+ const stream = sandboxDb.streams.createWriteStream({ clientId: streamClientId });
745
+ const streamId = typeof stream.streamId === "function" ? await stream.streamId() : streamClientId;
746
+ const writer = stream.getWriter();
747
+ const now = Date.now();
748
+ const metadata = {
749
+ source: "codex.commandExecution",
750
+ codexItemId,
751
+ providerThreadId: asString(params.threadId),
752
+ providerTurnId: asString(params.turnId),
753
+ parent: "codex-app-server",
754
+ commandActions: item.commandActions,
755
+ observed: true,
756
+ lastSeq: 1,
757
+ chunkCount: 1,
758
+ };
759
+ await sandboxDb.transact([
760
+ sandboxDb.tx.sandbox_processes[processId]
761
+ .update({
762
+ kind: "command",
763
+ mode: "foreground",
764
+ status: "running",
765
+ provider,
766
+ command: asString(item.command),
767
+ args: [],
768
+ cwd: asString(item.cwd) || repoPath,
769
+ externalProcessId: asString(item.processId) || undefined,
770
+ streamId,
771
+ streamClientId,
772
+ streamStartedAt: now,
773
+ startedAt: now,
774
+ updatedAt: now,
775
+ metadata,
776
+ })
777
+ .link({ sandbox: sandboxId, stream: streamId }),
778
+ ]);
779
+ const statusChunk = {
780
+ version: 1,
781
+ at: new Date().toISOString(),
782
+ seq: 1,
783
+ type: "status",
784
+ sandboxId,
785
+ processId,
786
+ data: {
787
+ status: "running",
788
+ command: asString(item.command),
789
+ args: [],
790
+ cwd: asString(item.cwd) || repoPath,
791
+ externalProcessId: asString(item.processId) || null,
792
+ },
793
+ };
794
+ await writer.write(`${JSON.stringify(statusChunk)}\n`);
795
+ observedCommandProcesses.set(codexItemId, {
796
+ processId,
797
+ streamId,
798
+ streamClientId,
799
+ writer,
800
+ seq: 1,
801
+ });
802
+ return;
803
+ }
804
+ if (method === "item/commandExecution/outputDelta") {
805
+ const codexItemId = asString(params.itemId);
806
+ const observed = observedCommandProcesses.get(codexItemId);
807
+ if (!observed)
808
+ return;
809
+ observed.seq += 1;
810
+ await observed.writer.write(`${JSON.stringify({
811
+ version: 1,
812
+ at: new Date().toISOString(),
813
+ seq: observed.seq,
814
+ type: "stdout",
815
+ sandboxId,
816
+ processId: observed.processId,
817
+ data: {
818
+ text: asString(params.delta),
819
+ source: "codex.commandExecution",
820
+ codexItemId,
821
+ },
822
+ })}\n`);
823
+ return;
824
+ }
825
+ if (method === "item/completed") {
826
+ const item = asRecord(params.item);
827
+ if (asString(item.type) !== "commandExecution")
828
+ return;
829
+ const codexItemId = asString(item.id);
830
+ const observed = observedCommandProcesses.get(codexItemId);
831
+ if (!observed)
832
+ return;
833
+ const aggregatedOutput = asString(item.aggregatedOutput);
834
+ if (aggregatedOutput) {
835
+ observed.seq += 1;
836
+ await observed.writer.write(`${JSON.stringify({
837
+ version: 1,
838
+ at: new Date().toISOString(),
839
+ seq: observed.seq,
840
+ type: "stdout",
841
+ sandboxId,
842
+ processId: observed.processId,
843
+ data: {
844
+ text: aggregatedOutput,
845
+ source: "codex.commandExecution",
846
+ codexItemId,
847
+ aggregated: true,
848
+ },
849
+ })}\n`);
850
+ }
851
+ const exitCode = typeof item.exitCode === "number" ? item.exitCode : Number(item.exitCode ?? 0);
852
+ const status = asString(item.status) === "failed" || exitCode !== 0 ? "failed" : "exited";
853
+ observed.seq += 1;
854
+ await observed.writer.write(`${JSON.stringify({
855
+ version: 1,
856
+ at: new Date().toISOString(),
857
+ seq: observed.seq,
858
+ type: "exit",
859
+ sandboxId,
860
+ processId: observed.processId,
861
+ data: {
862
+ exitCode: Number.isFinite(exitCode) ? exitCode : null,
863
+ status,
864
+ },
865
+ })}\n`);
866
+ await observed.writer.close();
867
+ observed.writer.releaseLock();
868
+ await sandboxDb.transact([
869
+ sandboxDb.tx.sandbox_processes[observed.processId].update({
870
+ status,
871
+ ...(Number.isFinite(exitCode) ? { exitCode } : {}),
872
+ streamFinishedAt: Date.now(),
873
+ streamAbortReason: asString(item.error) || null,
874
+ exitedAt: Date.now(),
875
+ updatedAt: Date.now(),
876
+ metadata: {
877
+ source: "codex.commandExecution",
878
+ codexItemId,
879
+ providerThreadId: asString(params.threadId),
880
+ providerTurnId: asString(params.turnId),
881
+ durationMs: item.durationMs,
882
+ completed: item,
883
+ },
884
+ }),
885
+ ]);
886
+ observedCommandProcesses.delete(codexItemId);
887
+ }
888
+ };
889
+ ensureOk(await actions.installCodexAuth({
890
+ sandboxId,
891
+ codexHome,
892
+ authJsonPath: sandboxConfig.authJsonPath,
893
+ credentialsJsonPath: sandboxConfig.credentialsJsonPath,
894
+ configTomlPath: sandboxConfig.configTomlPath,
895
+ }), "codex_sandbox_auth");
896
+ ensureOk(await actions.writeFiles({
897
+ sandboxId,
898
+ files: [
899
+ {
900
+ path: bridgePath,
901
+ contentBase64: Buffer.from(codexSandboxBridgeScript(), "utf8").toString("base64"),
902
+ },
903
+ {
904
+ path: turnRunnerPath,
905
+ contentBase64: Buffer.from(codexSandboxTurnRunnerScript(), "utf8").toString("base64"),
906
+ },
907
+ {
908
+ path: instructionPath,
909
+ contentBase64: Buffer.from(args.instruction, "utf8").toString("base64"),
910
+ },
911
+ ],
912
+ }), "codex_sandbox_write_files");
913
+ const runProcess = async (label, script, kind = "command", requiredText) => {
914
+ const result = ensureOk(await actions.runCommandProcess({
915
+ sandboxId,
916
+ command: "sh",
917
+ args: ["-lc", script],
918
+ kind,
919
+ mode: "foreground",
920
+ metadata: { source: "codex-reactor", label },
921
+ }), label);
922
+ if (requiredText) {
923
+ const output = stripProviderControlChars(`${asString(asRecord(result.result).output)}\n${asString(asRecord(result.result).error)}`);
924
+ if (!output.includes(requiredText)) {
925
+ throw new Error(`${label}: missing_sentinel:${requiredText}:${output.slice(-1000)}`);
926
+ }
927
+ }
928
+ return result;
929
+ };
930
+ await runProcess("codex_sandbox_prepare_codex", [
931
+ "set -euo pipefail",
932
+ `mkdir -p ${shellSingleQuote(codexHome)} ${shellSingleQuote(workRoot)}`,
933
+ `chmod 700 ${shellSingleQuote(codexHome)} || true`,
934
+ `chmod 600 ${shellSingleQuote(`${codexHome}/auth.json`)} 2>/dev/null || true`,
935
+ "if ! command -v codex >/dev/null 2>&1; then npm i -g @openai/codex@latest; fi",
936
+ `HOME=${shellSingleQuote(homeDir)} CODEX_HOME=${shellSingleQuote(codexHome)} codex login status`,
937
+ "echo codex_sandbox_prepare_codex_ok",
938
+ ].join("\n"), "command", "codex_sandbox_prepare_codex_ok");
939
+ if (sandboxConfig.checkpoint !== false) {
940
+ const checkpoint = await actions.createCheckpoint({
941
+ sandboxId,
942
+ comment: "codex auth and cli ready",
943
+ });
944
+ if (checkpoint.ok) {
945
+ checkpoints.push({ label: "codex-ready", checkpointId: String(checkpoint.data.checkpointId) });
946
+ }
947
+ }
948
+ await runProcess("codex_sandbox_start_bridge", [
949
+ "set -euo pipefail",
950
+ `if ! curl -fsS http://127.0.0.1:${bridgePort}/health >/dev/null 2>&1; then`,
951
+ ` HOME=${shellSingleQuote(homeDir)} CODEX_HOME=${shellSingleQuote(codexHome)} CODEX_BRIDGE_PORT=${bridgePort} nohup node ${shellSingleQuote(bridgePath)} > /tmp/ekairos-codex-bridge-${bridgePort}.log 2>&1 &`,
952
+ ` echo $! > /tmp/ekairos-codex-bridge-${bridgePort}.pid`,
953
+ "fi",
954
+ `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`,
955
+ `cat /tmp/ekairos-codex-bridge-${bridgePort}.log || true`,
956
+ "exit 1",
957
+ ].join("\n"), "codex-app-server", "codex_sandbox_bridge_ok");
958
+ if (sandboxConfig.createApp) {
959
+ const createdApp = ensureOk(await actions.createEkairosApp({
960
+ sandboxId,
961
+ appDir: repoPath,
962
+ packageManager: "pnpm",
963
+ instantTokenEnvName: "INSTANT_PERSONAL_ACCESS_TOKEN",
964
+ }), "codex_sandbox_create_app");
965
+ const createdAppOutput = stripProviderControlChars(asString(asRecord(createdApp.result).output));
966
+ if (!createdAppOutput.includes("sandbox_create_ekairos_app_ok")) {
967
+ throw new Error(`codex_sandbox_create_app: missing_sentinel:${createdAppOutput.slice(-1000)}`);
968
+ }
969
+ }
970
+ if (sandboxConfig.installApp) {
971
+ await runProcess("codex_sandbox_install_app", [
972
+ "set -euo pipefail",
973
+ `cd ${shellSingleQuote(repoPath)}`,
974
+ "for i in 1 2 3; do npx -y pnpm@10.15.1 install && break; echo pnpm_install_retry_$i; sleep 20; done",
975
+ "test -x node_modules/.bin/next",
976
+ "echo codex_sandbox_install_app_ok",
977
+ ].join("\n"), "command", "codex_sandbox_install_app_ok");
978
+ }
979
+ let appBaseUrl = "";
980
+ if (sandboxConfig.startApp) {
981
+ await runProcess("codex_sandbox_start_app", [
982
+ "set -euo pipefail",
983
+ `cd ${shellSingleQuote(repoPath)}`,
984
+ `if ! curl -fsS http://127.0.0.1:${appPort}/api/ekairos/domain >/dev/null 2>&1; then`,
985
+ ` nohup npx -y pnpm@10.15.1 dev --hostname 0.0.0.0 --port ${appPort} > /tmp/ekairos-app-${appPort}.log 2>&1 &`,
986
+ ` echo $! > /tmp/ekairos-app-${appPort}.pid`,
987
+ "fi",
988
+ `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`,
989
+ `cat /tmp/ekairos-app-${appPort}.log || true`,
990
+ "exit 1",
991
+ ].join("\n"), "dev-server", "codex_sandbox_start_app_ok");
992
+ const portUrl = ensureOk(await actions.getPortUrl({ sandboxId, port: appPort }), "codex_sandbox_port_url");
993
+ appBaseUrl = String(portUrl.url ?? "").replace(/\/+$/, "");
994
+ if (appBaseUrl) {
995
+ const response = await fetch(`${appBaseUrl}/api/ekairos/domain`);
996
+ if (!response.ok)
997
+ throw new Error(`codex_sandbox_app_url_unavailable_${response.status}`);
998
+ }
999
+ }
1000
+ if (sandboxConfig.checkpoint !== false && (sandboxConfig.createApp || sandboxConfig.installApp || sandboxConfig.startApp)) {
1001
+ const checkpoint = await actions.createCheckpoint({
1002
+ sandboxId,
1003
+ comment: "codex reactor app ready",
1004
+ });
1005
+ if (checkpoint.ok) {
1006
+ checkpoints.push({ label: "app-ready", checkpointId: String(checkpoint.data.checkpointId) });
1007
+ }
1008
+ }
1009
+ const bridgeUrl = ensureOk(await actions.getPortUrl({ sandboxId, port: bridgePort }), "codex_sandbox_bridge_url");
1010
+ const bridgeBaseUrl = String(bridgeUrl.url ?? "").replace(/\/+$/, "");
1011
+ if (!bridgeBaseUrl)
1012
+ throw new Error("codex_sandbox_bridge_url_missing");
1013
+ const turn = await executeCodexHttpTurn({
1014
+ ...args,
1015
+ systemPrompt: args.systemPrompt,
1016
+ config: {
1017
+ ...args.config,
1018
+ mode: "remote",
1019
+ appServerUrl: bridgeBaseUrl,
1020
+ repoPath,
1021
+ },
1022
+ actions: args.actions,
1023
+ actionSpecs: args.actionSpecs,
1024
+ context: args.context,
1025
+ storedContext: args.storedContext,
1026
+ contextIdentifier: args.contextIdentifier,
1027
+ }, {
1028
+ ...helpers,
1029
+ emitProviderChunk: emitAndObserveProviderChunk,
1030
+ }, bridgeBaseUrl);
1031
+ return {
1032
+ providerContextId: turn.providerContextId,
1033
+ turnId: turn.turnId,
1034
+ assistantText: turn.assistantText,
1035
+ reasoningText: turn.reasoningText,
1036
+ diff: turn.diff,
1037
+ toolParts: turn.toolParts,
1038
+ usage: turn.usage,
1039
+ metadata: {
1040
+ provider: "codex-sandbox",
1041
+ dynamicTools: asUnknownArray(asRecord(turn.metadata).dynamicTools),
1042
+ sandbox: {
1043
+ sandboxId,
1044
+ repoPath,
1045
+ appBaseUrl,
1046
+ bridgeBaseUrl,
1047
+ bridgePort,
1048
+ appPort,
1049
+ processId: "",
1050
+ streamId: "",
1051
+ streamClientId: "",
1052
+ checkpoints,
1053
+ },
1054
+ providerResponse: asRecord(turn.metadata).providerResponse,
1055
+ streamTrace: helpers.streamTrace(),
1056
+ },
1057
+ };
1058
+ }
1059
+ async function readJsonResponse(response) {
1060
+ const text = await response.text().catch(() => "");
1061
+ if (!text.trim())
1062
+ return {};
1063
+ try {
1064
+ return asRecord(JSON.parse(text));
1065
+ }
1066
+ catch {
1067
+ return {};
1068
+ }
1069
+ }
1070
+ async function codexAppServerRpc(baseUrl, method, params) {
1071
+ const response = await fetch(`${baseUrl}/rpc`, {
1072
+ method: "POST",
1073
+ headers: { "content-type": "application/json" },
1074
+ body: JSON.stringify({ method, params }),
1075
+ });
1076
+ const payload = await readJsonResponse(response);
1077
+ if (!response.ok) {
1078
+ const error = asString(payload.error) || asString(asRecord(payload.error).message);
1079
+ throw new Error(error || `codex_rpc_http_${response.status}`);
1080
+ }
1081
+ if (payload.error) {
1082
+ const error = asString(payload.error) || asString(asRecord(payload.error).message);
1083
+ throw new Error(error || "codex_rpc_error");
1084
+ }
1085
+ return payload;
1086
+ }
1087
+ async function executeCodexHttpTurn(args, helpers, baseUrl) {
1088
+ const eventsResponse = await fetch(`${baseUrl}/events`, {
1089
+ method: "GET",
1090
+ headers: { accept: "text/event-stream" },
1091
+ });
1092
+ if (!eventsResponse.ok || !eventsResponse.body) {
1093
+ throw new Error(`codex_events_unavailable_${eventsResponse.status}`);
1094
+ }
1095
+ const dynamicTools = buildCodexDynamicTools(args.actionSpecs);
1096
+ const baseInstructions = asString(args.systemPrompt).trim();
1097
+ const requestedThreadId = asString(args.config.providerContextId).trim();
1098
+ let providerContextId = requestedThreadId;
1099
+ if (providerContextId && isValidProviderContextId(providerContextId)) {
1100
+ await codexAppServerRpc(baseUrl, "thread/resume", { threadId: providerContextId });
1101
+ }
1102
+ else {
1103
+ const startParams = {
1104
+ cwd: args.config.repoPath,
1105
+ approvalPolicy: args.config.approvalPolicy ?? "never",
1106
+ sandboxPolicy: args.config.sandboxPolicy && Object.keys(args.config.sandboxPolicy).length > 0
1107
+ ? args.config.sandboxPolicy
1108
+ : { type: "externalSandbox", networkAccess: "enabled" },
1109
+ ...(dynamicTools.length > 0 ? { dynamicTools, dynamic_tools: dynamicTools } : {}),
1110
+ ...(dynamicTools.length > 0
1111
+ ? { experimentalRawEvents: true, persistExtendedHistory: true }
1112
+ : {}),
1113
+ ...(baseInstructions ? { baseInstructions } : {}),
1114
+ };
1115
+ if (args.config.model)
1116
+ startParams.model = args.config.model;
1117
+ const started = await codexAppServerRpc(baseUrl, "thread/start", startParams);
1118
+ providerContextId =
1119
+ asString(asRecord(asRecord(started.result).thread).id) ||
1120
+ asString(asRecord(started.result).id) ||
1121
+ asString(started.threadId);
1122
+ }
1123
+ if (!providerContextId)
1124
+ throw new Error("codex_thread_id_missing");
1125
+ const turnParams = {
1126
+ threadId: providerContextId,
1127
+ input: [{ type: "text", text: args.instruction }],
1128
+ cwd: args.config.repoPath,
1129
+ approvalPolicy: args.config.approvalPolicy ?? "never",
1130
+ sandboxPolicy: args.config.sandboxPolicy && Object.keys(args.config.sandboxPolicy).length > 0
1131
+ ? args.config.sandboxPolicy
1132
+ : { type: "externalSandbox", networkAccess: "enabled" },
1133
+ ...(dynamicTools.length > 0 ? { dynamicTools, dynamic_tools: dynamicTools } : {}),
1134
+ };
1135
+ if (args.config.model)
1136
+ turnParams.model = args.config.model;
1137
+ const turnStart = await codexAppServerRpc(baseUrl, "turn/start", turnParams);
1138
+ let turnId = asString(asRecord(asRecord(turnStart.result).turn).id) ||
1139
+ asString(asRecord(turnStart.result).id) ||
1140
+ asString(turnStart.turnId);
1141
+ const reader = eventsResponse.body.getReader();
1142
+ const decoder = new TextDecoder();
1143
+ let buffer = "";
1144
+ let assistantText = "";
1145
+ let reasoningText = "";
1146
+ let diff = "";
1147
+ let usage = {};
1148
+ let completedTurn = {};
1149
+ const isScopedToTurn = (evt) => {
1150
+ const params = asRecord(evt.params);
1151
+ const evtTurnId = asString(params.turnId) || asString(asRecord(params.turn).id);
1152
+ const evtThreadId = asString(params.threadId) ||
1153
+ asString(params.providerContextId) ||
1154
+ asString(asRecord(params.turn).threadId) ||
1155
+ asString(asRecord(params.turn).providerContextId);
1156
+ return ((evtTurnId && turnId && evtTurnId === turnId) ||
1157
+ (evtThreadId && evtThreadId === providerContextId) ||
1158
+ asString(evt.method).startsWith("thread/") ||
1159
+ asString(evt.method).startsWith("context/"));
1160
+ };
1161
+ try {
1162
+ while (true) {
1163
+ const read = await reader.read();
1164
+ if (read.done)
1165
+ break;
1166
+ if (!read.value)
1167
+ continue;
1168
+ buffer += decoder.decode(read.value, { stream: true });
1169
+ const blocks = buffer.split("\n\n");
1170
+ buffer = blocks.pop() ?? "";
1171
+ for (const block of blocks) {
1172
+ const data = parseSseDataBlock(block);
1173
+ if (!data || data === "[DONE]")
1174
+ continue;
1175
+ const evt = asRecord(JSON.parse(data));
1176
+ const method = asString(evt.method);
1177
+ if (!method)
1178
+ continue;
1179
+ if (method === "item/tool/call" && evt.id !== undefined && evt.id !== null) {
1180
+ if (!isScopedToTurn(evt))
1181
+ continue;
1182
+ const toolParams = asRecord(evt.params);
1183
+ await helpers.emitProviderChunk(evt);
1184
+ const executed = await executeCodexDynamicToolCall(args, toolParams);
1185
+ await helpers.emitProviderChunk({
1186
+ method: "item/tool/result",
1187
+ params: {
1188
+ ...toolParams,
1189
+ result: executed.response,
1190
+ output: executed.output,
1191
+ success: executed.success,
1192
+ errorText: executed.errorText,
1193
+ },
1194
+ });
1195
+ await codexAppServerRespond(baseUrl, {
1196
+ id: evt.id,
1197
+ result: executed.response,
1198
+ });
1199
+ continue;
1200
+ }
1201
+ const params = asRecord(evt.params);
1202
+ if (!isScopedToTurn(evt))
1203
+ continue;
1204
+ await helpers.emitProviderChunk(evt);
1205
+ if (method === "turn/started" && !turnId) {
1206
+ turnId = asString(asRecord(params.turn).id) || asString(params.turnId);
1207
+ }
1208
+ if (method === "item/agentMessage/delta") {
1209
+ assistantText += asString(params.delta);
1210
+ }
1211
+ if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
1212
+ reasoningText += asString(params.delta);
1213
+ }
1214
+ if (method === "turn/diff/updated") {
1215
+ diff = asString(params.diff);
1216
+ }
1217
+ if (method === "thread/tokenUsage/updated" || method === "context/tokenUsage/updated") {
1218
+ usage = asRecord(params.tokenUsage);
1219
+ }
1220
+ if (method === "item/completed") {
1221
+ const item = asRecord(params.item);
1222
+ if (asString(item.type) === "agentMessage" && asString(item.text).trim()) {
1223
+ assistantText = asString(item.text);
1224
+ }
1225
+ if (asString(item.type) === "reasoning" && asString(item.summary).trim()) {
1226
+ reasoningText = asString(item.summary);
1227
+ }
1228
+ }
1229
+ if (method === "turn/completed") {
1230
+ completedTurn = asRecord(params.turn);
1231
+ return {
1232
+ providerContextId,
1233
+ turnId: asString(completedTurn.id) || turnId,
1234
+ assistantText,
1235
+ reasoningText,
1236
+ diff,
1237
+ toolParts: asUnknownArray(completedTurn.toolParts),
1238
+ usage,
1239
+ metadata: {
1240
+ provider: "codex-app-server",
1241
+ providerResponse: completedTurn,
1242
+ dynamicTools: dynamicTools.map((tool) => asString(tool.name)).filter(Boolean),
1243
+ streamTrace: helpers.streamTrace(),
1244
+ },
1245
+ };
1246
+ }
1247
+ if (method === "turn/failed") {
1248
+ const evtTurnId = asString(params.turnId) || asString(asRecord(params.turn).id);
1249
+ throw new Error(`codex_turn_failed_${evtTurnId || turnId || "unknown"}`);
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ finally {
1255
+ await reader.cancel().catch(() => { });
1256
+ }
1257
+ throw new Error("codex_turn_completion_missing");
1258
+ }
1259
+ export async function executeCodexAppServerTurnStep(args) {
1260
+ "use step";
1261
+ const baseUrl = normalizeAppServerBaseUrl(args.config.appServerUrl);
1262
+ if (!baseUrl)
1263
+ throw new Error("codex_app_server_url_required");
1264
+ let sequence = 0;
1265
+ const mappedChunks = [];
1266
+ const chunkTypeCounters = new Map();
1267
+ const providerChunkTypeCounters = new Map();
1268
+ const contextWriter = args.contextStepStream?.getWriter();
1269
+ const workflowWriter = args.writable?.getWriter();
1270
+ const emitProviderChunk = async (providerChunk) => {
1271
+ const mapped = defaultMapCodexChunk(providerChunk);
1272
+ if (!mapped || mapped.skip)
1273
+ return;
1274
+ sequence += 1;
1275
+ const mappedChunk = {
1276
+ at: new Date().toISOString(),
1277
+ sequence,
1278
+ chunkType: mapped.chunkType,
1279
+ providerChunkType: mapped.providerChunkType,
1280
+ actionRef: mapped.actionRef,
1281
+ data: mapped.data,
1282
+ raw: mapped.raw ?? toJsonSafe(providerChunk),
1283
+ };
1284
+ mappedChunks.push(mappedChunk);
1285
+ chunkTypeCounters.set(mappedChunk.chunkType, (chunkTypeCounters.get(mappedChunk.chunkType) ?? 0) + 1);
1286
+ const providerType = mappedChunk.providerChunkType || "unknown";
1287
+ providerChunkTypeCounters.set(providerType, (providerChunkTypeCounters.get(providerType) ?? 0) + 1);
1288
+ const payload = {
1289
+ at: mappedChunk.at,
1290
+ sequence,
1291
+ chunkType: mappedChunk.chunkType,
1292
+ provider: "codex",
1293
+ providerChunkType: mappedChunk.providerChunkType,
1294
+ actionRef: mappedChunk.actionRef,
1295
+ data: mappedChunk.data,
1296
+ raw: mappedChunk.raw,
1297
+ };
1298
+ await contextWriter?.write(encodeContextStepStreamChunk(createContextStepStreamChunk(payload)));
1299
+ await workflowWriter?.write({
1300
+ type: "data-chunk.emitted",
1301
+ data: {
1302
+ type: "chunk.emitted",
1303
+ contextId: args.contextId,
1304
+ executionId: args.executionId,
1305
+ stepId: args.stepId,
1306
+ itemId: args.eventId,
1307
+ ...payload,
1308
+ },
1309
+ });
1310
+ };
1311
+ const streamTrace = () => ({
1312
+ totalChunks: mappedChunks.length,
1313
+ chunkTypes: Object.fromEntries(chunkTypeCounters.entries()),
1314
+ providerChunkTypes: Object.fromEntries(providerChunkTypeCounters.entries()),
1315
+ chunks: mappedChunks,
1316
+ });
1317
+ try {
1318
+ if (args.config.mode === "sandbox" || args.config.sandbox) {
1319
+ return await executeCodexSandboxTurn(args, {
1320
+ emitProviderChunk,
1321
+ streamTrace,
1322
+ });
1323
+ }
1324
+ if (String(args.config.appServerUrl || "").trim().replace(/\/+$/, "").endsWith("/turn")) {
1325
+ const response = await fetch(args.config.appServerUrl, {
1326
+ method: "POST",
1327
+ headers: { "content-type": "application/json" },
1328
+ body: JSON.stringify({
1329
+ instruction: args.instruction,
1330
+ config: args.config,
1331
+ runtime: { source: "openai-reactor" },
1332
+ }),
1333
+ });
1334
+ const payload = await readJsonResponse(response);
1335
+ if (!response.ok) {
1336
+ throw new Error(asString(payload.error) || `codex_turn_http_${response.status}`);
1337
+ }
1338
+ for (const chunk of asUnknownArray(payload.stream)) {
1339
+ await emitProviderChunk(chunk);
1340
+ }
1341
+ return {
1342
+ providerContextId: asString(payload.providerContextId) ||
1343
+ asString(payload.contextId) ||
1344
+ asString(args.config.providerContextId),
1345
+ turnId: asString(payload.turnId),
1346
+ assistantText: asString(payload.assistantText) || asString(payload.text),
1347
+ reasoningText: asString(payload.reasoningText) || asString(payload.reasoning),
1348
+ diff: asString(payload.diff),
1349
+ toolParts: asUnknownArray(payload.toolParts),
1350
+ usage: asRecord(payload.usage),
1351
+ metadata: {
1352
+ provider: "codex-app-server",
1353
+ response: payload,
1354
+ streamTrace: streamTrace(),
1355
+ },
1356
+ };
1357
+ }
1358
+ return await executeCodexHttpTurn(args, {
1359
+ emitProviderChunk,
1360
+ streamTrace,
1361
+ }, baseUrl);
1362
+ const eventsResponse = await fetch(`${baseUrl}/events`, {
1363
+ method: "GET",
1364
+ headers: { accept: "text/event-stream" },
1365
+ });
1366
+ if (!eventsResponse.ok || !eventsResponse.body) {
1367
+ throw new Error(`codex_events_unavailable_${eventsResponse.status}`);
1368
+ }
1369
+ const requestedThreadId = asString(args.config.providerContextId).trim();
1370
+ let providerContextId = requestedThreadId;
1371
+ if (providerContextId && isValidProviderContextId(providerContextId)) {
1372
+ await codexAppServerRpc(baseUrl, "thread/resume", { threadId: providerContextId });
1373
+ }
1374
+ else {
1375
+ const startParams = {
1376
+ cwd: args.config.repoPath,
1377
+ approvalPolicy: args.config.approvalPolicy ?? "never",
1378
+ sandboxPolicy: args.config.sandboxPolicy && Object.keys(args.config.sandboxPolicy ?? {}).length > 0
1379
+ ? args.config.sandboxPolicy
1380
+ : { type: "externalSandbox", networkAccess: "enabled" },
1381
+ };
1382
+ if (args.config.model)
1383
+ startParams.model = args.config.model;
1384
+ const started = await codexAppServerRpc(baseUrl, "thread/start", startParams);
1385
+ providerContextId =
1386
+ asString(asRecord(asRecord(started.result).thread).id) ||
1387
+ asString(asRecord(started.result).id) ||
1388
+ asString(started.threadId);
1389
+ }
1390
+ if (!providerContextId)
1391
+ throw new Error("codex_thread_id_missing");
1392
+ const turnParams = {
1393
+ threadId: providerContextId,
1394
+ input: [{ type: "text", text: args.instruction }],
1395
+ cwd: args.config.repoPath,
1396
+ approvalPolicy: args.config.approvalPolicy ?? "never",
1397
+ sandboxPolicy: args.config.sandboxPolicy && Object.keys(args.config.sandboxPolicy ?? {}).length > 0
1398
+ ? args.config.sandboxPolicy
1399
+ : { type: "externalSandbox", networkAccess: "enabled" },
1400
+ };
1401
+ if (args.config.model)
1402
+ turnParams.model = args.config.model;
1403
+ const turnStart = await codexAppServerRpc(baseUrl, "turn/start", turnParams);
1404
+ let turnId = asString(asRecord(asRecord(turnStart.result).turn).id) ||
1405
+ asString(asRecord(turnStart.result).id) ||
1406
+ asString(turnStart.turnId);
1407
+ const reader = eventsResponse.body.getReader();
1408
+ const decoder = new TextDecoder();
1409
+ let buffer = "";
1410
+ let assistantText = "";
1411
+ let reasoningText = "";
1412
+ let diff = "";
1413
+ let usage = {};
1414
+ let completedTurn = {};
1415
+ try {
1416
+ while (true) {
1417
+ const read = await reader.read();
1418
+ if (read.done)
1419
+ break;
1420
+ if (!read.value)
1421
+ continue;
1422
+ buffer += decoder.decode(read.value, { stream: true });
1423
+ const blocks = buffer.split("\n\n");
1424
+ buffer = blocks.pop() ?? "";
1425
+ for (const block of blocks) {
1426
+ const data = parseSseDataBlock(block);
1427
+ if (!data || data === "[DONE]")
1428
+ continue;
1429
+ const evt = asRecord(JSON.parse(String(data)));
1430
+ const method = asString(evt.method);
1431
+ if (!method)
1432
+ continue;
1433
+ const params = asRecord(evt.params);
1434
+ const evtTurnId = asString(params.turnId) || asString(asRecord(params.turn).id);
1435
+ const evtThreadId = asString(params.threadId) ||
1436
+ asString(params.providerContextId) ||
1437
+ asString(asRecord(params.turn).threadId) ||
1438
+ asString(asRecord(params.turn).providerContextId);
1439
+ const scopedToTurn = (evtTurnId && turnId && evtTurnId === turnId) ||
1440
+ (evtThreadId && evtThreadId === providerContextId) ||
1441
+ method.startsWith("thread/") ||
1442
+ method.startsWith("context/");
1443
+ if (!scopedToTurn)
1444
+ continue;
1445
+ await emitProviderChunk(evt);
1446
+ if (method === "turn/started" && !turnId) {
1447
+ turnId = asString(asRecord(params.turn).id) || evtTurnId;
1448
+ }
1449
+ if (method === "item/agentMessage/delta") {
1450
+ assistantText += asString(params.delta);
1451
+ }
1452
+ if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
1453
+ reasoningText += asString(params.delta);
1454
+ }
1455
+ if (method === "turn/diff/updated") {
1456
+ diff = asString(params.diff);
1457
+ }
1458
+ if (method === "thread/tokenUsage/updated" || method === "context/tokenUsage/updated") {
1459
+ usage = asRecord(params.tokenUsage);
1460
+ }
1461
+ if (method === "item/completed") {
1462
+ const item = asRecord(params.item);
1463
+ if (asString(item.type) === "agentMessage" && asString(item.text).trim()) {
1464
+ assistantText = asString(item.text);
1465
+ }
1466
+ if (asString(item.type) === "reasoning" && asString(item.summary).trim()) {
1467
+ reasoningText = asString(item.summary);
1468
+ }
1469
+ }
1470
+ if (method === "turn/completed") {
1471
+ completedTurn = asRecord(params.turn);
1472
+ return {
1473
+ providerContextId,
1474
+ turnId: asString(completedTurn.id) || turnId,
1475
+ assistantText,
1476
+ reasoningText,
1477
+ diff,
1478
+ toolParts: asUnknownArray(completedTurn.toolParts),
1479
+ usage,
1480
+ metadata: {
1481
+ provider: "codex-app-server",
1482
+ providerResponse: completedTurn,
1483
+ streamTrace: streamTrace(),
1484
+ },
1485
+ };
1486
+ }
1487
+ if (method === "turn/failed") {
1488
+ throw new Error(`codex_turn_failed_${evtTurnId || turnId || "unknown"}`);
1489
+ }
1490
+ }
1491
+ }
1492
+ }
1493
+ finally {
1494
+ await reader.cancel().catch(() => { });
1495
+ }
1496
+ throw new Error("codex_turn_completion_missing");
1497
+ }
1498
+ finally {
1499
+ contextWriter?.releaseLock();
1500
+ workflowWriter?.releaseLock();
1501
+ }
1502
+ }
281
1503
  /**
282
1504
  * Codex App Server reactor for @ekairos/events.
283
1505
  *
@@ -298,7 +1520,6 @@ export function createCodexReactor(options) {
298
1520
  const maxPersistedStreamChunks = Math.max(0, Number(options.maxPersistedStreamChunks ?? 300));
299
1521
  return async (params) => {
300
1522
  let chunkSequence = 0;
301
- const contextStepStreamWriter = params.contextStepStream?.getWriter();
302
1523
  const chunkTypeCounters = new Map();
303
1524
  const providerChunkTypeCounters = new Map();
304
1525
  const capturedChunks = [];
@@ -322,6 +1543,27 @@ export function createCodexReactor(options) {
322
1543
  stepId: params.stepId,
323
1544
  iteration: params.iteration,
324
1545
  });
1546
+ const persistedReactor = asRecord(params.context.reactor);
1547
+ const persistedReactorState = asRecord(persistedReactor.state);
1548
+ if (!config.providerContextId) {
1549
+ const providerContextId = asString(persistedReactorState.providerContextId);
1550
+ if (providerContextId)
1551
+ config.providerContextId = providerContextId;
1552
+ }
1553
+ if (config.sandbox) {
1554
+ const sandboxState = asRecord(persistedReactorState.sandbox);
1555
+ if (!config.sandbox.sandboxId) {
1556
+ const sandboxId = asString(sandboxState.sandboxId);
1557
+ if (sandboxId)
1558
+ config.sandbox.sandboxId = sandboxId;
1559
+ }
1560
+ if (!config.repoPath) {
1561
+ const repoPath = asString(sandboxState.repoPath);
1562
+ if (repoPath)
1563
+ config.repoPath = repoPath;
1564
+ }
1565
+ }
1566
+ const effectiveActionSpecs = toCodexActionSpecs(params.actionSpecs ?? asRecord(params.toolsForModel));
325
1567
  const startedAtMs = Date.now();
326
1568
  let streamedAssistantText = "";
327
1569
  let streamedReasoningText = "";
@@ -333,6 +1575,8 @@ export function createCodexReactor(options) {
333
1575
  const mappedMethod = asString(mappedData.method);
334
1576
  if (mappedMethod !== "item/started" &&
335
1577
  mappedMethod !== "item/completed" &&
1578
+ mappedMethod !== "item/tool/call" &&
1579
+ mappedMethod !== "item/tool/result" &&
336
1580
  mappedMethod !== "thread/tokenUsage/updated" &&
337
1581
  mappedMethod !== "context/tokenUsage/updated" &&
338
1582
  mappedMethod !== "turn/completed" &&
@@ -452,8 +1696,14 @@ export function createCodexReactor(options) {
452
1696
  data: mappedChunk.data,
453
1697
  raw: mapped.raw ?? toJsonSafe(providerChunk),
454
1698
  };
455
- if (contextStepStreamWriter) {
456
- await contextStepStreamWriter.write(encodeContextStepStreamChunk(createContextStepStreamChunk(payload)));
1699
+ if (params.contextStepStream) {
1700
+ const writer = params.contextStepStream.getWriter();
1701
+ try {
1702
+ await writer.write(encodeContextStepStreamChunk(createContextStepStreamChunk(payload)));
1703
+ }
1704
+ finally {
1705
+ writer.releaseLock();
1706
+ }
457
1707
  }
458
1708
  if (params.writable) {
459
1709
  const writer = params.writable.getWriter();
@@ -475,9 +1725,10 @@ export function createCodexReactor(options) {
475
1725
  }
476
1726
  }
477
1727
  };
478
- try {
479
- const turn = await options.executeTurn({
1728
+ const turn = options.executeTurn
1729
+ ? await options.executeTurn({
480
1730
  env: params.env,
1731
+ runtime: params.runtime,
481
1732
  context,
482
1733
  triggerEvent: params.triggerEvent,
483
1734
  contextId: params.contextId,
@@ -487,71 +1738,116 @@ export function createCodexReactor(options) {
487
1738
  iteration: params.iteration,
488
1739
  instruction,
489
1740
  config,
1741
+ actions: params.actions,
1742
+ actionSpecs: effectiveActionSpecs,
490
1743
  skills: params.skills,
1744
+ storedContext: params.context,
1745
+ contextIdentifier: params.contextIdentifier,
1746
+ contextStepStream: params.contextStepStream,
491
1747
  writable: params.writable,
492
1748
  silent: params.silent,
493
1749
  emitChunk,
1750
+ })
1751
+ : await executeCodexAppServerTurnStep({
1752
+ config,
1753
+ env: params.env,
1754
+ runtime: params.runtime,
1755
+ instruction,
1756
+ systemPrompt: params.systemPrompt,
1757
+ contextId: params.contextId,
1758
+ eventId: params.eventId,
1759
+ executionId: params.executionId,
1760
+ stepId: params.stepId,
1761
+ iteration: params.iteration,
1762
+ context,
1763
+ actions: params.actions,
1764
+ actionSpecs: effectiveActionSpecs,
1765
+ storedContext: params.context,
1766
+ contextIdentifier: params.contextIdentifier,
1767
+ contextStepStream: params.contextStepStream,
1768
+ writable: params.writable,
1769
+ silent: params.silent,
494
1770
  });
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
- }),
1771
+ const finishedAtMs = Date.now();
1772
+ const returnedStreamTrace = asRecord(asRecord(turn.metadata).streamTrace);
1773
+ const returnedChunks = Array.isArray(returnedStreamTrace.chunks)
1774
+ ? returnedStreamTrace.chunks
1775
+ : [];
1776
+ const effectiveRawChunks = allCapturedChunks.length > 0 ? allCapturedChunks : returnedChunks;
1777
+ const effectiveSemanticChunks = semanticChunks.length > 0 ? semanticChunks : returnedChunks;
1778
+ const returnedChunkTypes = asNumberRecord(returnedStreamTrace.chunkTypes);
1779
+ const returnedProviderChunkTypes = asNumberRecord(returnedStreamTrace.providerChunkTypes);
1780
+ const returnedTotalChunks = typeof returnedStreamTrace.totalChunks === "number"
1781
+ ? returnedStreamTrace.totalChunks
1782
+ : returnedChunks.length;
1783
+ const streamTrace = includeStreamTraceInOutput
1784
+ ? {
1785
+ totalChunks: chunkSequence || returnedTotalChunks,
1786
+ chunkTypes: chunkSequence > 0
1787
+ ? Object.fromEntries(chunkTypeCounters.entries())
1788
+ : returnedChunkTypes,
1789
+ providerChunkTypes: chunkSequence > 0
1790
+ ? Object.fromEntries(providerChunkTypeCounters.entries())
1791
+ : returnedProviderChunkTypes,
1792
+ }
1793
+ : undefined;
1794
+ const usagePayload = toJsonSafe(turn.usage ?? asRecord(turn.metadata).usage);
1795
+ const usageMetrics = extractUsageMetrics(usagePayload);
1796
+ const assistantEvent = {
1797
+ id: params.eventId,
1798
+ type: OUTPUT_ITEM_TYPE,
1799
+ channel: "web",
1800
+ createdAt: new Date().toISOString(),
1801
+ status: "completed",
1802
+ content: {
1803
+ parts: buildCodexParts({
1804
+ toolName,
1805
+ includeReasoningPart,
1806
+ semanticChunks: effectiveSemanticChunks,
1807
+ rawChunks: effectiveRawChunks,
1808
+ result: turn,
1809
+ instruction,
1810
+ streamTrace,
1811
+ }),
1812
+ },
1813
+ };
1814
+ return {
1815
+ assistantEvent,
1816
+ actionRequests: [],
1817
+ messagesForModel: [],
1818
+ reactor: {
1819
+ kind: "codex",
1820
+ state: {
1821
+ providerContextId: turn.providerContextId,
1822
+ lastTurnId: turn.turnId,
1823
+ provider: asString(asRecord(turn.metadata).provider || "codex"),
1824
+ sandbox: asRecord(asRecord(turn.metadata).sandbox),
549
1825
  },
550
- };
551
- }
552
- finally {
553
- contextStepStreamWriter?.releaseLock();
554
- }
1826
+ },
1827
+ llm: {
1828
+ provider: "codex",
1829
+ model: asString(config.model || "codex"),
1830
+ promptTokens: usageMetrics.promptTokens,
1831
+ promptTokensCached: usageMetrics.promptTokensCached,
1832
+ promptTokensUncached: usageMetrics.promptTokensUncached,
1833
+ completionTokens: usageMetrics.completionTokens,
1834
+ totalTokens: usageMetrics.totalTokens,
1835
+ latencyMs: Math.max(0, finishedAtMs - startedAtMs),
1836
+ rawUsage: usagePayload,
1837
+ rawProviderMetadata: toJsonSafe({
1838
+ providerContextId: turn.providerContextId,
1839
+ turnId: turn.turnId,
1840
+ metadata: turn.metadata ?? null,
1841
+ streamTrace: streamTrace
1842
+ ? {
1843
+ totalChunks: streamTrace.totalChunks,
1844
+ chunkTypes: streamTrace.chunkTypes,
1845
+ providerChunkTypes: streamTrace.providerChunkTypes,
1846
+ }
1847
+ : undefined,
1848
+ }),
1849
+ },
1850
+ };
555
1851
  };
556
1852
  }
557
1853
  //# sourceMappingURL=codex.reactor.js.map