@ai-sdk/harness-codex 1.0.23 → 1.0.25
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 +17 -0
- package/dist/bridge/index.mjs +144 -31
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +35 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/codex-step-tracker.ts +83 -0
- package/src/bridge/index.ts +36 -27
- package/src/codex-harness.ts +42 -52
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @ai-sdk/harness-codex
|
|
2
2
|
|
|
3
|
+
## 1.0.25
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @ai-sdk/harness@1.0.24
|
|
8
|
+
|
|
9
|
+
## 1.0.24
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 39c8276: fix(harness): improve opaque sandbox bridge error handling
|
|
14
|
+
- 91fe6d8: fix(harness): emit `finish-step` stream parts correctly per the underlying model steps
|
|
15
|
+
- Updated dependencies [39c8276]
|
|
16
|
+
- Updated dependencies [91fe6d8]
|
|
17
|
+
- Updated dependencies [0be5014]
|
|
18
|
+
- @ai-sdk/harness@1.0.23
|
|
19
|
+
|
|
3
20
|
## 1.0.23
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/dist/bridge/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
451
|
+
emitError({ error: err, message: "uncaught exception" });
|
|
387
452
|
});
|
|
388
453
|
process.on("unhandledRejection", (err) => {
|
|
389
|
-
|
|
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
|
-
|
|
973
|
+
stepTracker,
|
|
974
|
+
setTurnUsage: (u) => turnUsage = u,
|
|
975
|
+
emitWarning: turn.emitWarning,
|
|
976
|
+
emitError: turn.emitError
|
|
858
977
|
});
|
|
859
978
|
}
|
|
860
979
|
} catch (err) {
|
|
861
|
-
|
|
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.
|
|
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.
|
|
907
|
-
|
|
908
|
-
|
|
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.
|
|
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
|
-
|
|
1027
|
-
|
|
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);
|