@mcoda/agents 0.1.35 → 0.1.37

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 +1 @@
1
- {"version":3,"file":"CodexAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/codex/CodexAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtG,qBAAa,YAAa,YAAW,YAAY;IACnC,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAEnC,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAIpC,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC;IAYnC,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAmB5D,YAAY,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAAC,gBAAgB,EAAE,IAAI,EAAE,OAAO,CAAC;CAoBjG"}
1
+ {"version":3,"file":"CodexAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/codex/CodexAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAmBtG,qBAAa,YAAa,YAAW,YAAY;IACnC,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAEnC,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAIpC,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC;IAYnC,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAwB5D,YAAY,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAAC,gBAAgB,EAAE,IAAI,EAAE,OAAO,CAAC;CAyBjG"}
@@ -1,4 +1,18 @@
1
1
  import { cliHealthy, runCodexExec, runCodexExecStream } from "./CodexCliRunner.js";
2
+ const extractOutputSchema = (request) => {
3
+ const candidate = request.metadata?.outputSchema;
4
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
5
+ return undefined;
6
+ }
7
+ return candidate;
8
+ };
9
+ const extractTimeoutMs = (request) => {
10
+ const candidate = request.metadata?.timeoutMs;
11
+ if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate <= 0) {
12
+ return undefined;
13
+ }
14
+ return Math.floor(candidate);
15
+ };
2
16
  export class CodexAdapter {
3
17
  constructor(config) {
4
18
  this.config = config;
@@ -20,7 +34,7 @@ export class CodexAdapter {
20
34
  async invoke(request) {
21
35
  const health = cliHealthy(true);
22
36
  const cliDetails = health.details;
23
- const result = runCodexExec(request.input, this.config.model);
37
+ const result = await runCodexExec(request.input, this.config.model, extractOutputSchema(request), extractTimeoutMs(request));
24
38
  return {
25
39
  output: result.output,
26
40
  adapter: this.config.adapter ?? "codex-cli",
@@ -38,7 +52,7 @@ export class CodexAdapter {
38
52
  async *invokeStream(request) {
39
53
  const health = cliHealthy(true);
40
54
  const cliDetails = health.details;
41
- for await (const chunk of runCodexExecStream(request.input, this.config.model)) {
55
+ for await (const chunk of runCodexExecStream(request.input, this.config.model, extractOutputSchema(request), extractTimeoutMs(request))) {
42
56
  yield {
43
57
  output: chunk.output,
44
58
  adapter: this.config.adapter ?? "codex-cli",
@@ -2,11 +2,11 @@ export declare const cliHealthy: (throwOnError?: boolean) => {
2
2
  ok: boolean;
3
3
  details?: Record<string, unknown>;
4
4
  };
5
- export declare const runCodexExec: (prompt: string, model?: string) => {
5
+ export declare const runCodexExec: (prompt: string, model?: string, outputSchema?: Record<string, unknown>, timeoutMs?: number) => Promise<{
6
6
  output: string;
7
7
  raw: string;
8
- };
9
- export declare function runCodexExecStream(prompt: string, model?: string): AsyncGenerator<{
8
+ }>;
9
+ export declare function runCodexExecStream(prompt: string, model?: string, outputSchema?: Record<string, unknown>, timeoutMs?: number): AsyncGenerator<{
10
10
  output: string;
11
11
  raw: string;
12
12
  }, void, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"CodexCliRunner.d.ts","sourceRoot":"","sources":["../../../src/adapters/codex/CodexCliRunner.ts"],"names":[],"mappings":"AA4hBA,eAAO,MAAM,UAAU,GAAI,sBAAoB,KAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CA2BjG,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,EAAE,QAAQ,MAAM,KAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAuD1F,CAAC;AAEF,wBAAuB,kBAAkB,CACvC,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,GACb,cAAc,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CA+GhE"}
1
+ {"version":3,"file":"CodexCliRunner.d.ts","sourceRoot":"","sources":["../../../src/adapters/codex/CodexCliRunner.ts"],"names":[],"mappings":"AAk1BA,eAAO,MAAM,UAAU,GAAI,sBAAoB,KAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CA+BjG,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,QAAQ,MAAM,EACd,QAAQ,MAAM,EACd,eAAe,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,YAAY,MAAM,KACjB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CASzC,CAAC;AAEF,wBAAuB,kBAAkB,CACvC,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,SAAS,CAAC,EAAE,MAAM,GACjB,cAAc,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAmFhE"}
@@ -1,4 +1,7 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
2
5
  const CODEX_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
3
6
  const CODEX_REASONING_ENV = "MCODA_CODEX_REASONING_EFFORT";
4
7
  const CODEX_REASONING_ENV_FALLBACK = "CODEX_REASONING_EFFORT";
@@ -7,6 +10,12 @@ const CODEX_STREAM_IO_ENV = "MCODA_STREAM_IO";
7
10
  const CODEX_STREAM_IO_FORMAT_ENV = "MCODA_STREAM_IO_FORMAT";
8
11
  const CODEX_STREAM_IO_COLOR_ENV = "MCODA_STREAM_IO_COLOR";
9
12
  const CODEX_STREAM_IO_PREFIX = "codex-cli";
13
+ const CODEX_COMMAND_ENV = "MCODA_CODEX_COMMAND";
14
+ const CODEX_COMMAND_ARGS_ENV = "MCODA_CODEX_COMMAND_ARGS";
15
+ const CODEX_TIMEOUT_ENV = "MCODA_CODEX_TIMEOUT_MS";
16
+ const CODEX_EXIT_GRACE_ENV = "MCODA_CODEX_EXIT_GRACE_MS";
17
+ const CODEX_DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
18
+ const CODEX_DEFAULT_EXIT_GRACE_MS = 5000;
10
19
  const ANSI = {
11
20
  reset: "\u001b[0m",
12
21
  bold: "\u001b[1m",
@@ -114,6 +123,95 @@ const extractItemText = (item) => {
114
123
  }
115
124
  return "";
116
125
  };
126
+ const parseCommandArgs = (raw) => {
127
+ const trimmed = raw?.trim();
128
+ if (!trimmed)
129
+ return [];
130
+ if (trimmed.startsWith("[")) {
131
+ try {
132
+ const parsed = JSON.parse(trimmed);
133
+ if (Array.isArray(parsed)) {
134
+ return parsed
135
+ .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
136
+ .filter(Boolean);
137
+ }
138
+ }
139
+ catch {
140
+ // fall through to whitespace parsing
141
+ }
142
+ }
143
+ return trimmed.split(/\s+/).filter(Boolean);
144
+ };
145
+ const resolveCodexCommand = () => {
146
+ const command = process.env[CODEX_COMMAND_ENV]?.trim() || "codex";
147
+ const preArgs = parseCommandArgs(process.env[CODEX_COMMAND_ARGS_ENV]);
148
+ return { command, preArgs };
149
+ };
150
+ const parsePositiveIntEnv = (name, fallback) => {
151
+ const raw = process.env[name]?.trim();
152
+ if (!raw)
153
+ return fallback;
154
+ const parsed = Number(raw);
155
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
156
+ };
157
+ const resolveCodexTimeoutMs = () => parsePositiveIntEnv(CODEX_TIMEOUT_ENV, CODEX_DEFAULT_TIMEOUT_MS);
158
+ const resolveCodexExitGraceMs = () => parsePositiveIntEnv(CODEX_EXIT_GRACE_ENV, CODEX_DEFAULT_EXIT_GRACE_MS);
159
+ const isIgnorableStdinError = (error) => error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED";
160
+ const safeKill = (child, signal) => {
161
+ try {
162
+ if (!child.killed) {
163
+ child.kill(signal);
164
+ }
165
+ }
166
+ catch {
167
+ /* ignore kill errors */
168
+ }
169
+ };
170
+ const scheduleHardKill = (child, delayMs = 250) => {
171
+ const timer = setTimeout(() => {
172
+ safeKill(child, "SIGKILL");
173
+ }, delayMs);
174
+ timer.unref();
175
+ };
176
+ const extractProtocolError = (parsed) => {
177
+ if (!parsed || typeof parsed !== "object")
178
+ return undefined;
179
+ const type = typeof parsed.type === "string" ? parsed.type : "";
180
+ if (type === "turn.failed") {
181
+ const message = parsed?.error?.message ?? parsed?.error;
182
+ return message ? String(message) : "codex turn failed";
183
+ }
184
+ if (type === "error" && parsed?.message) {
185
+ return String(parsed.message);
186
+ }
187
+ return undefined;
188
+ };
189
+ const isCompletionEvent = (parsed) => {
190
+ if (!parsed || typeof parsed !== "object")
191
+ return false;
192
+ const type = typeof parsed.type === "string" ? parsed.type : "";
193
+ return type === "turn.completed" || type === "response.completed";
194
+ };
195
+ const parseCodexOutput = (raw) => {
196
+ const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
197
+ let message = "";
198
+ for (const line of lines) {
199
+ const parsed = safeJsonParse(line);
200
+ const event = extractAssistantText(parsed);
201
+ if (!event)
202
+ continue;
203
+ if (event.kind === "delta") {
204
+ message += event.text;
205
+ continue;
206
+ }
207
+ message = event.text;
208
+ }
209
+ if (!message) {
210
+ return lines[lines.length - 1] ?? "";
211
+ }
212
+ return message;
213
+ };
214
+ const normalizeComparableAssistantText = (value) => value.replace(/\r\n/g, "\n").trim();
117
215
  const normalizeValue = (value) => {
118
216
  if (typeof value !== "string")
119
217
  return value;
@@ -541,6 +639,185 @@ const extractAssistantText = (parsed) => {
541
639
  }
542
640
  return null;
543
641
  };
642
+ const materializeOutputSchema = async (outputSchema) => {
643
+ if (!outputSchema || typeof outputSchema !== "object" || Array.isArray(outputSchema)) {
644
+ return undefined;
645
+ }
646
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "mcoda-codex-schema-"));
647
+ const schemaPath = path.join(tempDir, "output-schema.json");
648
+ await writeFile(schemaPath, `${JSON.stringify(outputSchema, null, 2)}\n`, "utf8");
649
+ return {
650
+ schemaPath,
651
+ cleanup: async () => {
652
+ try {
653
+ await rm(tempDir, { recursive: true, force: true });
654
+ }
655
+ catch {
656
+ /* ignore cleanup errors */
657
+ }
658
+ },
659
+ };
660
+ };
661
+ const invokeCodexProcess = async (prompt, model, options) => {
662
+ const resolvedModel = model ?? "gpt-5.1-codex-max";
663
+ const sandboxArgs = resolveSandboxArgs();
664
+ const reasoningEffort = resolveReasoningEffort(resolvedModel);
665
+ const codexCommand = resolveCodexCommand();
666
+ const args = [...codexCommand.preArgs, ...sandboxArgs.args, "exec", "--model", resolvedModel, "--json"];
667
+ const schemaFile = await materializeOutputSchema(options?.outputSchema);
668
+ if (schemaFile) {
669
+ args.push("--output-schema", schemaFile.schemaPath);
670
+ }
671
+ if (!sandboxArgs.bypass) {
672
+ args.push("--full-auto");
673
+ }
674
+ if (reasoningEffort) {
675
+ args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
676
+ }
677
+ args.push("-");
678
+ const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
679
+ ? Math.floor(options.timeoutMs)
680
+ : resolveCodexTimeoutMs();
681
+ const exitGraceMs = resolveCodexExitGraceMs();
682
+ try {
683
+ return await new Promise((resolve, reject) => {
684
+ const child = spawn(codexCommand.command, args, { stdio: ["pipe", "pipe", "pipe"] });
685
+ let raw = "";
686
+ let stderr = "";
687
+ let lineBuffer = "";
688
+ let message = "";
689
+ let protocolError;
690
+ let settled = false;
691
+ let forcedExit = false;
692
+ let completionTimer;
693
+ const clearTimers = () => {
694
+ if (completionTimer) {
695
+ clearTimeout(completionTimer);
696
+ completionTimer = undefined;
697
+ }
698
+ clearTimeout(timeoutHandle);
699
+ };
700
+ const finalizeOutput = () => {
701
+ const parsedOutput = parseCodexOutput(raw).trim();
702
+ return parsedOutput || message.trim();
703
+ };
704
+ const finishResolve = () => {
705
+ if (settled)
706
+ return;
707
+ settled = true;
708
+ clearTimers();
709
+ resolve({ output: finalizeOutput(), raw, forcedExit });
710
+ };
711
+ const finishReject = (error) => {
712
+ if (settled)
713
+ return;
714
+ settled = true;
715
+ clearTimers();
716
+ reject(error);
717
+ };
718
+ const scheduleCompletionGrace = () => {
719
+ if (settled)
720
+ return;
721
+ if (completionTimer) {
722
+ clearTimeout(completionTimer);
723
+ }
724
+ completionTimer = setTimeout(() => {
725
+ if (settled)
726
+ return;
727
+ forcedExit = true;
728
+ safeKill(child, "SIGTERM");
729
+ scheduleHardKill(child);
730
+ finishResolve();
731
+ }, exitGraceMs);
732
+ completionTimer.unref();
733
+ };
734
+ const handleLine = (line) => {
735
+ const normalized = line.replace(/\r$/, "");
736
+ options?.hooks?.onLine?.(normalized);
737
+ const parsed = safeJsonParse(normalized);
738
+ const event = extractAssistantText(parsed);
739
+ if (event) {
740
+ if (event.kind === "delta") {
741
+ message += event.text;
742
+ }
743
+ else {
744
+ message = event.text;
745
+ scheduleCompletionGrace();
746
+ }
747
+ options?.hooks?.onAssistantEvent?.(event, normalized);
748
+ }
749
+ const parsedError = extractProtocolError(parsed);
750
+ if (parsedError) {
751
+ protocolError = parsedError;
752
+ }
753
+ if (isCompletionEvent(parsed)) {
754
+ scheduleCompletionGrace();
755
+ }
756
+ };
757
+ const timeoutHandle = setTimeout(() => {
758
+ if (settled)
759
+ return;
760
+ forcedExit = true;
761
+ safeKill(child, "SIGTERM");
762
+ scheduleHardKill(child);
763
+ const detail = stderr.trim().length > 0 ? `: ${stderr.trim()}` : "";
764
+ finishReject(new Error(`AUTH_ERROR: codex CLI timed out after ${timeoutMs}ms${detail}`));
765
+ }, timeoutMs);
766
+ timeoutHandle.unref();
767
+ child.stdout?.setEncoding("utf8");
768
+ child.stdout?.on("data", (chunk) => {
769
+ const text = chunk.toString();
770
+ raw += text;
771
+ lineBuffer += text;
772
+ let idx;
773
+ while ((idx = lineBuffer.indexOf("\n")) !== -1) {
774
+ const line = lineBuffer.slice(0, idx);
775
+ lineBuffer = lineBuffer.slice(idx + 1);
776
+ handleLine(line);
777
+ }
778
+ });
779
+ child.stderr?.setEncoding("utf8");
780
+ child.stderr?.on("data", (chunk) => {
781
+ stderr += chunk.toString();
782
+ });
783
+ child.stdin?.on("error", (error) => {
784
+ if (settled || isIgnorableStdinError(error))
785
+ return;
786
+ finishReject(new Error(`AUTH_ERROR: codex CLI stdin failed (${error.message})`));
787
+ });
788
+ child.on("error", (error) => {
789
+ finishReject(new Error(`AUTH_ERROR: codex CLI failed (${error.message})`));
790
+ });
791
+ child.on("close", (code) => {
792
+ if (lineBuffer.trim()) {
793
+ handleLine(lineBuffer);
794
+ lineBuffer = "";
795
+ }
796
+ if (settled)
797
+ return;
798
+ if ((code ?? 0) !== 0) {
799
+ finishReject(new Error(`AUTH_ERROR: codex CLI failed (exit ${code ?? 0}): ${stderr || protocolError || "no output"}`));
800
+ return;
801
+ }
802
+ if (protocolError && !finalizeOutput()) {
803
+ finishReject(new Error(`AUTH_ERROR: codex CLI failed (${protocolError})`));
804
+ return;
805
+ }
806
+ finishResolve();
807
+ });
808
+ try {
809
+ child.stdin?.write(prompt);
810
+ child.stdin?.end();
811
+ }
812
+ catch (error) {
813
+ finishReject(new Error(`AUTH_ERROR: codex CLI stdin failed (${error.message})`));
814
+ }
815
+ });
816
+ }
817
+ finally {
818
+ await schemaFile?.cleanup();
819
+ }
820
+ };
544
821
  export const cliHealthy = (throwOnError = false) => {
545
822
  if (process.env.MCODA_CLI_STUB === "1") {
546
823
  return { ok: true, details: { stub: true } };
@@ -548,7 +825,11 @@ export const cliHealthy = (throwOnError = false) => {
548
825
  if (process.env.MCODA_SKIP_CLI_CHECKS === "1") {
549
826
  return { ok: true, details: { skipped: true } };
550
827
  }
551
- const result = spawnSync("codex", ["--version"], { encoding: "utf8", maxBuffer: CODEX_MAX_BUFFER_BYTES });
828
+ const codexCommand = resolveCodexCommand();
829
+ const result = spawnSync(codexCommand.command, [...codexCommand.preArgs, "--version"], {
830
+ encoding: "utf8",
831
+ maxBuffer: CODEX_MAX_BUFFER_BYTES,
832
+ });
552
833
  if (result.error) {
553
834
  const details = { reason: "missing_cli", error: result.error.message };
554
835
  if (throwOnError) {
@@ -569,64 +850,17 @@ export const cliHealthy = (throwOnError = false) => {
569
850
  }
570
851
  return { ok: true, details: { version: result.stdout?.toString().trim() } };
571
852
  };
572
- export const runCodexExec = (prompt, model) => {
853
+ export const runCodexExec = async (prompt, model, outputSchema, timeoutMs) => {
573
854
  if (process.env.MCODA_CLI_STUB === "1") {
574
855
  const output = `qa-stub:${prompt}`;
575
856
  const raw = JSON.stringify({ type: "item.completed", item: { type: "agent_message", text: output } });
576
857
  return { output, raw };
577
858
  }
578
- const health = cliHealthy(true);
579
- const resolvedModel = model ?? "gpt-5.1-codex-max";
580
- const sandboxArgs = resolveSandboxArgs();
581
- const args = [...sandboxArgs.args, "exec", "--model", resolvedModel, "--json"];
582
- if (!sandboxArgs.bypass) {
583
- args.push("--full-auto");
584
- }
585
- const reasoningEffort = resolveReasoningEffort(resolvedModel);
586
- if (reasoningEffort) {
587
- args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
588
- }
589
- args.push("-");
590
- const result = spawnSync("codex", args, {
591
- input: prompt,
592
- encoding: "utf8",
593
- maxBuffer: CODEX_MAX_BUFFER_BYTES,
594
- });
595
- if (result.error) {
596
- const error = new Error(`AUTH_ERROR: codex CLI failed (${result.error.message})`);
597
- error.details = { reason: "cli_error", cli: health.details };
598
- throw error;
599
- }
600
- if (result.status !== 0) {
601
- const error = new Error(`AUTH_ERROR: codex CLI failed (exit ${result.status}): ${result.stderr ?? result.stdout ?? ""}`);
602
- error.details = { reason: "cli_error", exitCode: result.status, stderr: result.stderr };
603
- throw error;
604
- }
605
- const raw = result.stdout?.toString() ?? "";
606
- const lines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
607
- let message = "";
608
- for (const line of lines) {
609
- try {
610
- const parsed = JSON.parse(line);
611
- const event = extractAssistantText(parsed);
612
- if (!event)
613
- continue;
614
- if (event.kind === "delta") {
615
- message += event.text;
616
- continue;
617
- }
618
- message = event.text;
619
- }
620
- catch {
621
- /* ignore parse errors */
622
- }
623
- }
624
- if (!message) {
625
- message = lines[lines.length - 1] ?? "";
626
- }
627
- return { output: message.trim(), raw };
859
+ cliHealthy(true);
860
+ const result = await invokeCodexProcess(prompt, model, { outputSchema, timeoutMs });
861
+ return { output: result.output, raw: result.raw };
628
862
  };
629
- export async function* runCodexExecStream(prompt, model) {
863
+ export async function* runCodexExecStream(prompt, model, outputSchema, timeoutMs) {
630
864
  if (process.env.MCODA_CLI_STUB === "1") {
631
865
  const output = `qa-stub:${prompt}\n`;
632
866
  const raw = JSON.stringify({ type: "item.delta", item: { type: "agent_message", text: output } });
@@ -635,103 +869,74 @@ export async function* runCodexExecStream(prompt, model) {
635
869
  }
636
870
  cliHealthy(true);
637
871
  const resolvedModel = model ?? "gpt-5.1-codex-max";
638
- const sandboxArgs = resolveSandboxArgs();
639
- const args = [...sandboxArgs.args, "exec", "--model", resolvedModel, "--json"];
640
- if (!sandboxArgs.bypass) {
641
- args.push("--full-auto");
642
- }
643
- const reasoningEffort = resolveReasoningEffort(resolvedModel);
644
- if (reasoningEffort) {
645
- args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
646
- }
647
- args.push("-");
648
- const child = spawn("codex", args, { stdio: ["pipe", "pipe", "pipe"] });
649
- child.stdin.write(prompt);
650
- child.stdin.end();
651
- let stderr = "";
652
- child.stderr?.setEncoding("utf8");
653
- child.stderr?.on("data", (chunk) => {
654
- stderr += chunk.toString();
655
- });
656
- const closePromise = new Promise((resolve, reject) => {
657
- child.on("error", (err) => reject(err));
658
- child.on("close", (code) => resolve(code ?? 0));
659
- });
660
- const parseLine = (line) => {
661
- try {
662
- const parsed = JSON.parse(line);
663
- return extractAssistantText(parsed);
664
- }
665
- catch {
666
- return null;
667
- }
668
- };
669
- const stream = child.stdout;
670
- stream?.setEncoding("utf8");
671
872
  const formatter = createStreamFormatter(resolvedModel);
672
- let buffer = "";
873
+ const queue = [];
874
+ const waiters = [];
875
+ let done = false;
876
+ let failure;
673
877
  let sawDelta = false;
674
- let streamError = null;
675
- try {
676
- for await (const chunk of stream ?? []) {
677
- buffer += chunk;
678
- let idx;
679
- while ((idx = buffer.indexOf("\n")) !== -1) {
680
- const line = buffer.slice(0, idx);
681
- buffer = buffer.slice(idx + 1);
682
- const normalized = line.replace(/\r$/, "");
683
- formatter.handleLine(normalized);
684
- const parsed = parseLine(normalized);
685
- if (!parsed)
686
- continue;
687
- if (parsed.kind === "delta") {
878
+ let assistantDeltaBuffer = "";
879
+ const notify = () => {
880
+ while (waiters.length) {
881
+ waiters.shift()?.();
882
+ }
883
+ };
884
+ void invokeCodexProcess(prompt, model, {
885
+ outputSchema,
886
+ timeoutMs,
887
+ hooks: {
888
+ onLine: (line) => {
889
+ formatter.handleLine(line);
890
+ },
891
+ onAssistantEvent: (event, rawLine) => {
892
+ if (event.kind === "delta") {
688
893
  sawDelta = true;
689
- yield { output: parsed.text, raw: normalized };
690
- continue;
894
+ assistantDeltaBuffer += event.text;
895
+ queue.push({ output: event.text, raw: rawLine });
896
+ notify();
897
+ return;
691
898
  }
899
+ const finalText = event.text;
900
+ const bufferedText = assistantDeltaBuffer;
901
+ assistantDeltaBuffer = "";
692
902
  if (!sawDelta) {
693
- const output = parsed.text.endsWith("\n") ? parsed.text : `${parsed.text}\n`;
694
- yield { output, raw: normalized };
903
+ const output = finalText.endsWith("\n") ? finalText : `${finalText}\n`;
904
+ queue.push({ output, raw: rawLine });
905
+ notify();
906
+ return;
695
907
  }
696
908
  sawDelta = false;
697
- }
698
- }
699
- const trailing = buffer.replace(/\r$/, "");
700
- if (trailing) {
701
- formatter.handleLine(trailing);
702
- const parsed = parseLine(trailing);
703
- if (parsed) {
704
- if (parsed.kind === "delta") {
705
- sawDelta = true;
706
- yield { output: parsed.text, raw: trailing };
707
- }
708
- else if (!sawDelta) {
709
- const output = parsed.text.endsWith("\n") ? parsed.text : `${parsed.text}\n`;
710
- yield { output, raw: trailing };
711
- sawDelta = false;
909
+ const normalizedFinal = normalizeComparableAssistantText(finalText);
910
+ const normalizedBuffered = normalizeComparableAssistantText(bufferedText);
911
+ if (!normalizedFinal || normalizedFinal === normalizedBuffered) {
912
+ return;
712
913
  }
713
- }
714
- }
715
- }
716
- catch (error) {
717
- streamError = error;
718
- }
719
- const exitCode = await closePromise;
720
- if (exitCode !== 0) {
721
- formatter.handleLine(JSON.stringify({
722
- type: "error",
723
- message: `codex exec failed with exit ${exitCode}: ${stderr || "no output"}`,
724
- }));
725
- const error = new Error(`AUTH_ERROR: codex CLI failed (exit ${exitCode}): ${stderr || "no output"}`);
726
- error.details = { reason: "cli_error", exitCode, stderr };
914
+ const suffix = finalText.startsWith(bufferedText) ? finalText.slice(bufferedText.length) : finalText;
915
+ const output = suffix.endsWith("\n") ? suffix : `${suffix}\n`;
916
+ queue.push({ output, raw: rawLine });
917
+ notify();
918
+ },
919
+ },
920
+ })
921
+ .catch((error) => {
922
+ failure = error;
923
+ formatter.handleLine(JSON.stringify({ type: "error", message: failure.message }));
924
+ })
925
+ .finally(() => {
926
+ done = true;
727
927
  formatter.end();
728
- if (streamError) {
729
- throw streamError;
928
+ notify();
929
+ });
930
+ while (!done || queue.length > 0) {
931
+ if (queue.length > 0) {
932
+ yield queue.shift();
933
+ continue;
730
934
  }
731
- throw error;
935
+ await new Promise((resolve) => {
936
+ waiters.push(resolve);
937
+ });
732
938
  }
733
- formatter.end();
734
- if (streamError) {
735
- throw streamError;
939
+ if (failure) {
940
+ throw failure;
736
941
  }
737
942
  }
@@ -19,7 +19,7 @@ export class OpenAiCliAdapter {
19
19
  }
20
20
  async invoke(request) {
21
21
  const cliDetails = codexCliHealthy(true);
22
- const result = runCodexExec(request.input, this.config.model);
22
+ const result = await runCodexExec(request.input, this.config.model);
23
23
  return {
24
24
  output: result.output,
25
25
  adapter: this.config.adapter ?? "codex-cli",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcoda/agents",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "Agent registry and capabilities for mcoda.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,8 +30,8 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@mcoda/shared": "0.1.35",
34
- "@mcoda/db": "0.1.35"
33
+ "@mcoda/shared": "0.1.37",
34
+ "@mcoda/db": "0.1.37"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc -p tsconfig.json",