@ai-sdk/harness-codex 1.0.23 → 1.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @ai-sdk/harness-codex
2
2
 
3
+ ## 1.0.24
4
+
5
+ ### Patch Changes
6
+
7
+ - 39c8276: fix(harness): improve opaque sandbox bridge error handling
8
+ - 91fe6d8: fix(harness): emit `finish-step` stream parts correctly per the underlying model steps
9
+ - Updated dependencies [39c8276]
10
+ - Updated dependencies [91fe6d8]
11
+ - Updated dependencies [0be5014]
12
+ - @ai-sdk/harness@1.0.23
13
+
3
14
  ## 1.0.23
4
15
 
5
16
  ### Patch Changes
@@ -20,6 +20,15 @@ function formatBridgeError(err) {
20
20
  if (err instanceof Error) {
21
21
  return { name: err.name, message: err.message, stack: err.stack };
22
22
  }
23
+ if (typeof err === "string") {
24
+ return { message: err };
25
+ }
26
+ if (err !== null && typeof err === "object") {
27
+ try {
28
+ return { message: JSON.stringify(err) };
29
+ } catch {
30
+ }
31
+ }
23
32
  return { message: String(err) };
24
33
  }
25
34
  function parseEnvList(value) {
@@ -47,6 +56,7 @@ async function runBridge(options) {
47
56
  let isFirstTurn = true;
48
57
  let turnAbort;
49
58
  let currentUserMessages;
59
+ let currentInterruptHandler;
50
60
  let debugConfig;
51
61
  let consoleCaptureInstalled = false;
52
62
  const envDebugEnabled = ENV_TRUTHY.has(
@@ -164,6 +174,38 @@ async function runBridge(options) {
164
174
  };
165
175
  const rawStdoutWrite = process.stdout.write.bind(process.stdout);
166
176
  const rawStderrWrite = process.stderr.write.bind(process.stderr);
177
+ const writeErrorToStderr = (input) => {
178
+ try {
179
+ const formatted = formatBridgeError(input.error);
180
+ rawStderrWrite(
181
+ `[harness:${bridgeType}:error] ${input.message}: ${formatted.message}
182
+ `
183
+ );
184
+ if (formatted.stack) {
185
+ rawStderrWrite(`${formatted.stack}
186
+ `);
187
+ }
188
+ } catch {
189
+ }
190
+ };
191
+ const emitWarning = (input) => {
192
+ try {
193
+ for (const line of input.message.split("\n")) {
194
+ if (line.trim().length > 0) {
195
+ rawStderrWrite(`[harness:${bridgeType}:warn] ${line}
196
+ `);
197
+ }
198
+ }
199
+ } catch {
200
+ }
201
+ };
202
+ const emitError = (input) => {
203
+ writeErrorToStderr({
204
+ message: input.message ?? "bridge error",
205
+ error: input.error
206
+ });
207
+ emit({ type: "error", error: serialiseError(input.error) });
208
+ };
167
209
  const installConsoleCapture = () => {
168
210
  if (consoleCaptureInstalled) return;
169
211
  consoleCaptureInstalled = true;
@@ -221,6 +263,7 @@ async function runBridge(options) {
221
263
  });
222
264
  turnAbort = new AbortController();
223
265
  currentTurnState = "running";
266
+ currentInterruptHandler = void 0;
224
267
  void writeStartConfig(msg);
225
268
  void writeBridgeMeta("running");
226
269
  const startDebug = msg.debug;
@@ -242,6 +285,9 @@ async function runBridge(options) {
242
285
  }),
243
286
  pendingUserMessages: [],
244
287
  abortSignal: turnAbort.signal,
288
+ onInterrupt: (handler) => {
289
+ currentInterruptHandler = handler;
290
+ },
245
291
  firstTurn,
246
292
  bridgeLog: (input) => {
247
293
  const level = input.level ?? "debug";
@@ -254,14 +300,17 @@ async function runBridge(options) {
254
300
  ...input.attrs ? { attrs: input.attrs } : {},
255
301
  ...input.error !== void 0 ? { error: formatBridgeError(input.error) } : {}
256
302
  });
257
- }
303
+ },
304
+ emitWarning,
305
+ emitError
258
306
  };
259
307
  currentUserMessages = turn.pendingUserMessages;
260
308
  try {
261
309
  await onStart(msg, turn);
262
310
  } catch (err) {
263
- emit({ type: "error", error: serialiseError(err) });
311
+ emitError({ error: err, message: "bridge turn failed" });
264
312
  } finally {
313
+ currentInterruptHandler = void 0;
265
314
  currentTurnState = "waiting";
266
315
  void writeBridgeMeta("waiting");
267
316
  }
@@ -289,6 +338,22 @@ async function runBridge(options) {
289
338
  case "abort":
290
339
  turnAbort?.abort();
291
340
  return;
341
+ case "interrupt":
342
+ try {
343
+ if (currentInterruptHandler) {
344
+ await currentInterruptHandler();
345
+ } else {
346
+ turnAbort?.abort();
347
+ }
348
+ sendControl({ type: "bridge-interrupted", ok: true });
349
+ } catch (err) {
350
+ sendControl({
351
+ type: "bridge-interrupted",
352
+ ok: false,
353
+ error: serialiseError(err)
354
+ });
355
+ }
356
+ return;
292
357
  case "resume":
293
358
  replay(ws, msg.lastSeenEventId);
294
359
  return;
@@ -383,10 +448,10 @@ async function runBridge(options) {
383
448
  });
384
449
  });
385
450
  process.on("uncaughtException", (err) => {
386
- emit({ type: "error", error: serialiseError(err) });
451
+ emitError({ error: err, message: "uncaught exception" });
387
452
  });
388
453
  process.on("unhandledRejection", (err) => {
389
- emit({ type: "error", error: serialiseError(err) });
454
+ emitError({ error: err, message: "unhandled rejection" });
390
455
  });
391
456
  await new Promise((resolve, reject) => {
392
457
  if (wss.address() != null) {
@@ -555,6 +620,54 @@ function parseShellWords(command) {
555
620
  return words;
556
621
  }
557
622
 
623
+ // src/bridge/codex-step-tracker.ts
624
+ function createCodexStepTracker(input) {
625
+ let stepOpen = false;
626
+ const pendingToolItemIds = /* @__PURE__ */ new Set();
627
+ const finishStep = () => {
628
+ if (!stepOpen || pendingToolItemIds.size > 0) return;
629
+ input.send({
630
+ type: "finish-step",
631
+ finishReason: { unified: "stop", raw: "stop" },
632
+ usage: defaultUsage(),
633
+ harnessMetadata: { codex: { inferredStep: true } }
634
+ });
635
+ stepOpen = false;
636
+ };
637
+ return {
638
+ observeEvent({ event, itemId }) {
639
+ const item = event.item;
640
+ if (!item || !isStepItem(item)) return;
641
+ stepOpen = true;
642
+ if (isToolStepItem(item)) {
643
+ if (event.type === "item.started" && itemId) {
644
+ pendingToolItemIds.add(itemId);
645
+ } else if (event.type === "item.completed") {
646
+ if (itemId) pendingToolItemIds.delete(itemId);
647
+ finishStep();
648
+ }
649
+ return;
650
+ }
651
+ },
652
+ finishStep
653
+ };
654
+ }
655
+ function isStepItem(item) {
656
+ return isModelStepItem(item) || isToolStepItem(item);
657
+ }
658
+ function isModelStepItem(item) {
659
+ return item.type === "reasoning" || item.type === "agent_message";
660
+ }
661
+ function isToolStepItem(item) {
662
+ return item.type === "command_execution" || item.type === "mcp_tool_call" || item.type === "web_search" || item.type === "file_change" || item.type === "todo_list";
663
+ }
664
+ function defaultUsage() {
665
+ return {
666
+ inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
667
+ outputTokens: { total: 0, text: 0 }
668
+ };
669
+ }
670
+
558
671
  // src/bridge/tool-relay-auth.ts
559
672
  import { readdir, readFile, readlink } from "fs/promises";
560
673
  var ToolRelayAuthorizer = class {
@@ -819,6 +932,7 @@ async function runTurn(start, turn) {
819
932
  let turnUsage;
820
933
  const textByItem = /* @__PURE__ */ new Map();
821
934
  const reasoningByItem = /* @__PURE__ */ new Map();
935
+ const stepTracker = createCodexStepTracker({ send: emit });
822
936
  try {
823
937
  const { events } = await thread.runStreamed(userMessage, {
824
938
  signal: turn.abortSignal
@@ -842,10 +956,12 @@ async function runTurn(start, turn) {
842
956
  }
843
957
  }
844
958
  if (relayCall) {
959
+ stepTracker.observeEvent({ event, itemId: event.item.id });
845
960
  continue;
846
961
  }
847
962
  }
848
963
  if (relay && isHostMcpToolEvent(event)) {
964
+ stepTracker.observeEvent({ event, itemId: event.item?.id });
849
965
  const relayCall = relayCallFromCodexMcpEvent(event);
850
966
  if (relayCall) relay.authorizeToolCall(relayCall);
851
967
  continue;
@@ -854,11 +970,14 @@ async function runTurn(start, turn) {
854
970
  send: emit,
855
971
  textByItem,
856
972
  reasoningByItem,
857
- setTurnUsage: (u) => turnUsage = u
973
+ stepTracker,
974
+ setTurnUsage: (u) => turnUsage = u,
975
+ emitWarning: turn.emitWarning,
976
+ emitError: turn.emitError
858
977
  });
859
978
  }
860
979
  } catch (err) {
861
- emit({ type: "error", error: serialiseError2(err) });
980
+ turn.emitError({ error: err, message: "codex turn failed" });
862
981
  return;
863
982
  } finally {
864
983
  relay?.close();
@@ -895,27 +1014,29 @@ function relayCallFromCodexMcpEvent(event) {
895
1014
  function translateAndEmit(event, ctx) {
896
1015
  if (event.type === "turn.completed") {
897
1016
  if (event.usage) ctx.setTurnUsage(mapUsage(event.usage));
898
- ctx.send({
899
- type: "finish-step",
900
- finishReason: { unified: "stop", raw: "stop" },
901
- usage: event.usage ? mapUsage(event.usage) : defaultUsage()
902
- });
1017
+ ctx.stepTracker.finishStep();
903
1018
  return;
904
1019
  }
905
1020
  if (event.type === "turn.failed") {
906
- ctx.send({
907
- type: "error",
908
- error: event.error?.message ?? "codex turn failed"
1021
+ ctx.emitError({
1022
+ error: event.error?.message ?? "codex turn failed",
1023
+ message: "codex turn failed"
909
1024
  });
910
1025
  return;
911
1026
  }
912
1027
  if (event.type === "error") {
913
- ctx.send({ type: "error", error: event.message ?? "codex error" });
1028
+ ctx.emitError({
1029
+ error: event.message ?? "codex error",
1030
+ message: "codex stream error"
1031
+ });
914
1032
  return;
915
1033
  }
916
1034
  if (!event.item) return;
917
1035
  const item = event.item;
918
1036
  const id = item.id ?? randomUUID();
1037
+ const observeStep = () => {
1038
+ ctx.stepTracker.observeEvent({ event, itemId: id });
1039
+ };
919
1040
  if (item.type === "agent_message" && typeof item.text === "string") {
920
1041
  if (!ctx.textByItem.has(id)) {
921
1042
  ctx.send({ type: "text-start", id });
@@ -928,6 +1049,7 @@ function translateAndEmit(event, ctx) {
928
1049
  ctx.textByItem.set(id, next);
929
1050
  }
930
1051
  if (event.type === "item.completed") ctx.send({ type: "text-end", id });
1052
+ observeStep();
931
1053
  return;
932
1054
  }
933
1055
  if (item.type === "reasoning" && typeof item.text === "string") {
@@ -943,6 +1065,7 @@ function translateAndEmit(event, ctx) {
943
1065
  }
944
1066
  if (event.type === "item.completed")
945
1067
  ctx.send({ type: "reasoning-end", id });
1068
+ observeStep();
946
1069
  return;
947
1070
  }
948
1071
  if (item.type === "command_execution") {
@@ -968,6 +1091,7 @@ function translateAndEmit(event, ctx) {
968
1091
  }
969
1092
  });
970
1093
  }
1094
+ observeStep();
971
1095
  return;
972
1096
  }
973
1097
  if (item.type === "mcp_tool_call") {
@@ -989,6 +1113,7 @@ function translateAndEmit(event, ctx) {
989
1113
  result: extractMcpToolCallResult(item)
990
1114
  });
991
1115
  }
1116
+ observeStep();
992
1117
  return;
993
1118
  }
994
1119
  if (item.type === "web_search") {
@@ -1010,6 +1135,7 @@ function translateAndEmit(event, ctx) {
1010
1135
  result: item.result ?? null
1011
1136
  });
1012
1137
  }
1138
+ observeStep();
1013
1139
  return;
1014
1140
  }
1015
1141
  if (item.type === "file_change" && event.type === "item.completed") {
@@ -1020,13 +1146,12 @@ function translateAndEmit(event, ctx) {
1020
1146
  path: change.path
1021
1147
  });
1022
1148
  }
1149
+ observeStep();
1023
1150
  return;
1024
1151
  }
1025
1152
  if (item.type === "error" && event.type === "item.completed") {
1026
- ctx.send({
1027
- type: "error",
1028
- error: item.message ?? "codex item error"
1029
- });
1153
+ const message = typeof item.message === "string" && item.message.trim() ? item.message : "codex reported a non-fatal error item";
1154
+ ctx.emitWarning({ message });
1030
1155
  return;
1031
1156
  }
1032
1157
  }
@@ -1046,12 +1171,6 @@ function mapUsage(usage) {
1046
1171
  }
1047
1172
  };
1048
1173
  }
1049
- function defaultUsage() {
1050
- return {
1051
- inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
1052
- outputTokens: { total: 0, text: 0 }
1053
- };
1054
- }
1055
1174
  async function startToolRelay({
1056
1175
  allowedScriptPaths,
1057
1176
  tools,
@@ -1160,12 +1279,6 @@ function parseArgs(args2) {
1160
1279
  }
1161
1280
  return out;
1162
1281
  }
1163
- function serialiseError2(err) {
1164
- if (err instanceof Error) {
1165
- return { name: err.name, message: err.message, stack: err.stack };
1166
- }
1167
- return err;
1168
- }
1169
1282
  function emitFatal(message) {
1170
1283
  stdout2.write(JSON.stringify({ type: "bridge-fatal", message }) + "\n");
1171
1284
  process.exit(1);