@ai-sdk/harness-opencode 1.0.21 → 1.0.23

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,23 @@
1
1
  # @ai-sdk/harness-opencode
2
2
 
3
+ ## 1.0.23
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
+
14
+ ## 1.0.22
15
+
16
+ ### Patch Changes
17
+
18
+ - @ai-sdk/harness@1.0.22
19
+ - @ai-sdk/provider-utils@5.0.7
20
+
3
21
  ## 1.0.21
4
22
 
5
23
  ### 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) {
@@ -477,19 +542,6 @@ function asRecord(value) {
477
542
  return value;
478
543
  }
479
544
 
480
- // src/bridge/opencode-path.ts
481
- import path from "path";
482
- var fallbackPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
483
- function prependOpenCodeBinToPath({
484
- bootstrapDir: bootstrapDir2,
485
- env
486
- }) {
487
- env.PATH = [
488
- path.join(bootstrapDir2, "node_modules", ".bin"),
489
- env.PATH || fallbackPath
490
- ].join(path.delimiter);
491
- }
492
-
493
545
  // src/bridge/opencode-usage.ts
494
546
  function mapUsage(tokens) {
495
547
  const value = extractOpenCodeTokens(tokens) ?? zeroOpenCodeTokens();
@@ -608,6 +660,47 @@ function add({
608
660
  return leftNumber == null && rightNumber == null ? void 0 : (leftNumber ?? 0) + (rightNumber ?? 0);
609
661
  }
610
662
 
663
+ // src/bridge/opencode-finish-step.ts
664
+ function mapOpenCodeFinishReason(reason) {
665
+ const normalized = reason.toLowerCase();
666
+ if (normalized.includes("length")) return "length";
667
+ if (normalized.includes("filter")) return "content-filter";
668
+ if (normalized.includes("tool")) return "tool-calls";
669
+ if (normalized.includes("error") || normalized.includes("fail"))
670
+ return "error";
671
+ if (normalized === "stop" || normalized === "end") return "stop";
672
+ return "other";
673
+ }
674
+ function legacyStepFinishPartToFinishStep(part) {
675
+ if (!isRecord(part) || part.type !== "step-finish") return void 0;
676
+ const rawFinish = typeof part.reason === "string" ? part.reason : "stop";
677
+ return {
678
+ type: "finish-step",
679
+ finishReason: {
680
+ unified: mapOpenCodeFinishReason(rawFinish),
681
+ raw: rawFinish
682
+ },
683
+ usage: mapUsage(part.tokens),
684
+ ...typeof part.cost === "number" ? { harnessMetadata: { opencode: { cost: part.cost } } } : {}
685
+ };
686
+ }
687
+ function isRecord(value) {
688
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
689
+ }
690
+
691
+ // src/bridge/opencode-path.ts
692
+ import path from "path";
693
+ var fallbackPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
694
+ function prependOpenCodeBinToPath({
695
+ bootstrapDir: bootstrapDir2,
696
+ env
697
+ }) {
698
+ env.PATH = [
699
+ path.join(bootstrapDir2, "node_modules", ".bin"),
700
+ env.PATH || fallbackPath
701
+ ].join(path.delimiter);
702
+ }
703
+
611
704
  // src/bridge/tool-relay-auth.ts
612
705
  import { readdir, readFile, readlink } from "fs/promises";
613
706
  var ToolRelayAuthorizer = class {
@@ -790,7 +883,7 @@ async function runTurn(start, turn) {
790
883
  totalUsage = await runPrompt({ client, sessionId, start, turn, emit });
791
884
  }
792
885
  } catch (err) {
793
- emit({ type: "error", error: serialiseError2(err) });
886
+ turn.emitError({ error: err, message: "OpenCode turn failed" });
794
887
  } finally {
795
888
  emit({
796
889
  type: "finish",
@@ -1032,11 +1125,38 @@ function legacyStatusType(event) {
1032
1125
  const status = event.properties?.status;
1033
1126
  return status && typeof status === "object" ? String(status.type ?? "") : void 0;
1034
1127
  }
1035
- function legacyStatusMessage(event) {
1128
+ function legacyRetryStatusMessage(event) {
1036
1129
  const status = event.properties?.status;
1037
- if (!status || typeof status !== "object") return void 0;
1038
- const message = status.message;
1039
- return typeof message === "string" ? message : void 0;
1130
+ const details = [];
1131
+ if (status && typeof status === "object") {
1132
+ const retryStatus = status;
1133
+ if (typeof retryStatus.attempt === "number") {
1134
+ details.push(`attempt ${retryStatus.attempt}`);
1135
+ }
1136
+ if (typeof retryStatus.message === "string" && retryStatus.message.trim()) {
1137
+ details.push(retryStatus.message.trim());
1138
+ }
1139
+ }
1140
+ return details.length > 0 ? `OpenCode session retry: ${details.join("; ")}` : "OpenCode session retry";
1141
+ }
1142
+ function nextRetryEventMessage(event) {
1143
+ const props = event.properties ?? {};
1144
+ const details = [];
1145
+ if (typeof props.attempt === "number") {
1146
+ details.push(`attempt ${props.attempt}`);
1147
+ }
1148
+ const error = props.error;
1149
+ if (isRecord2(error)) {
1150
+ const message = stringValue(error.message) ?? (isRecord2(error.data) ? stringValue(error.data.message) : void 0);
1151
+ const statusCode = error.statusCode;
1152
+ if (typeof statusCode === "number") {
1153
+ details.push(`HTTP ${statusCode}`);
1154
+ }
1155
+ if (message) details.push(message);
1156
+ } else if (error != null) {
1157
+ details.push(formatError(error));
1158
+ }
1159
+ return details.length > 0 ? `OpenCode session retry: ${details.join("; ")}` : "OpenCode session retry";
1040
1160
  }
1041
1161
  async function ensureSession({
1042
1162
  client,
@@ -1119,9 +1239,7 @@ async function runPrompt({
1119
1239
  sawBusy = true;
1120
1240
  } else if (status === "retry") {
1121
1241
  sawBusy = true;
1122
- terminalError = legacyStatusMessage(event) ?? "Session retry";
1123
- turnSettled.resolve();
1124
- return true;
1242
+ turn.emitWarning({ message: legacyRetryStatusMessage(event) });
1125
1243
  } else if (sawBusy && status === "idle") {
1126
1244
  turnSettled.resolve();
1127
1245
  return true;
@@ -1221,9 +1339,7 @@ async function runCompaction({
1221
1339
  sawBusy = true;
1222
1340
  } else if (status === "retry") {
1223
1341
  sawBusy = true;
1224
- terminalError = legacyStatusMessage(event) ?? "Session retry";
1225
- compactionSettled.resolve();
1226
- return true;
1342
+ turn.emitWarning({ message: legacyRetryStatusMessage(event) });
1227
1343
  } else if (sawBusy && status === "idle") {
1228
1344
  compactionSettled.resolve();
1229
1345
  return true;
@@ -1306,7 +1422,8 @@ function createTranslationState() {
1306
1422
  messageRoles: /* @__PURE__ */ new Map(),
1307
1423
  turnUsage: void 0,
1308
1424
  legacyTextPartIds: /* @__PURE__ */ new Set(),
1309
- legacyReasoningPartIds: /* @__PURE__ */ new Set()
1425
+ legacyReasoningPartIds: /* @__PURE__ */ new Set(),
1426
+ legacyStepFinishPartIds: /* @__PURE__ */ new Set()
1310
1427
  };
1311
1428
  }
1312
1429
  async function translateAndEmit({
@@ -1323,7 +1440,7 @@ async function translateAndEmit({
1323
1440
  const props = event.properties ?? {};
1324
1441
  if (type === "message.updated") {
1325
1442
  const info = props.info;
1326
- if (isRecord(info)) {
1443
+ if (isRecord2(info)) {
1327
1444
  const id = stringValue(info.id);
1328
1445
  const role = stringValue(info.role);
1329
1446
  if (id && role) state.messageRoles.set(id, role);
@@ -1361,6 +1478,7 @@ async function translateAndEmit({
1361
1478
  }
1362
1479
  if (type === "message.part.updated") {
1363
1480
  if (emitLegacyTextPartUpdate({ part: props.part, state, emit })) return;
1481
+ if (emitLegacyStepFinishPart({ part: props.part, state, emit })) return;
1364
1482
  emitLegacyToolPart({ part: props.part, state, emit });
1365
1483
  return;
1366
1484
  }
@@ -1508,13 +1626,25 @@ async function translateAndEmit({
1508
1626
  });
1509
1627
  return;
1510
1628
  }
1629
+ if (type === "session.next.retried") {
1630
+ const error = props.error ?? event;
1631
+ if (isRecord2(error) && error.isRetryable === false) {
1632
+ turn.emitError({
1633
+ error,
1634
+ message: "OpenCode session retry failed"
1635
+ });
1636
+ } else {
1637
+ turn.emitWarning({ message: nextRetryEventMessage(event) });
1638
+ }
1639
+ return;
1640
+ }
1511
1641
  if (type === "session.next.step.ended") {
1512
1642
  closeLegacyOpenParts({ state, emit });
1513
1643
  state.turnUsage = mapUsage(props.tokens);
1514
1644
  emit({
1515
1645
  type: "finish-step",
1516
1646
  finishReason: {
1517
- unified: mapFinishReason(String(props.finish ?? "stop")),
1647
+ unified: mapOpenCodeFinishReason(String(props.finish ?? "stop")),
1518
1648
  raw: String(props.finish ?? "stop")
1519
1649
  },
1520
1650
  usage: state.turnUsage,
@@ -1544,7 +1674,11 @@ async function translateAndEmit({
1544
1674
  return;
1545
1675
  }
1546
1676
  if (type === "session.error" || type === "session.next.step.failed") {
1547
- emit({ type: "error", error: formatError(props.error ?? event) });
1677
+ const error = props.error ?? event;
1678
+ turn.emitError({
1679
+ error,
1680
+ message: type === "session.error" ? "OpenCode session error" : "OpenCode step failed"
1681
+ });
1548
1682
  return;
1549
1683
  }
1550
1684
  if (type === "permission.v2.asked") {
@@ -1592,7 +1726,7 @@ function emitLegacyTextPartUpdate({
1592
1726
  state,
1593
1727
  emit
1594
1728
  }) {
1595
- if (!isRecord(part)) return false;
1729
+ if (!isRecord2(part)) return false;
1596
1730
  if (part.type !== "text" && part.type !== "reasoning") return false;
1597
1731
  const id = stringValue(part.id);
1598
1732
  if (!id) return true;
@@ -1627,7 +1761,7 @@ function emitLegacyTextPartUpdate({
1627
1761
  return true;
1628
1762
  }
1629
1763
  function legacyPartEnded(part) {
1630
- return isRecord(part.time) && part.time.end != null;
1764
+ return isRecord2(part.time) && part.time.end != null;
1631
1765
  }
1632
1766
  function closeLegacyOpenParts({
1633
1767
  state,
@@ -1644,6 +1778,23 @@ function closeLegacyOpenParts({
1644
1778
  }
1645
1779
  state.legacyTextPartIds.clear();
1646
1780
  }
1781
+ function emitLegacyStepFinishPart({
1782
+ part,
1783
+ state,
1784
+ emit
1785
+ }) {
1786
+ const event = legacyStepFinishPartToFinishStep(part);
1787
+ if (!event) return false;
1788
+ const id = isRecord2(part) ? stringValue(part.id) : void 0;
1789
+ if (id) {
1790
+ if (state.legacyStepFinishPartIds.has(id)) return true;
1791
+ state.legacyStepFinishPartIds.add(id);
1792
+ }
1793
+ closeLegacyOpenParts({ state, emit });
1794
+ state.turnUsage = event.usage;
1795
+ emit(event);
1796
+ return true;
1797
+ }
1647
1798
  function emitLegacyToolPart({
1648
1799
  part,
1649
1800
  state,
@@ -1704,9 +1855,9 @@ function legacyToolPartStatus(part) {
1704
1855
  function legacyToolPartInput(part) {
1705
1856
  const state = typeof part.state === "object" && part.state !== null ? part.state : void 0;
1706
1857
  return {
1707
- ...isRecord(part.metadata) ? part.metadata : {},
1708
- ...isRecord(state?.metadata) ? state.metadata : {},
1709
- ...isRecord(state?.input) ? state.input : {}
1858
+ ...isRecord2(part.metadata) ? part.metadata : {},
1859
+ ...isRecord2(state?.metadata) ? state.metadata : {},
1860
+ ...isRecord2(state?.input) ? state.input : {}
1710
1861
  };
1711
1862
  }
1712
1863
  function legacyToolPartOutput(part) {
@@ -1716,7 +1867,7 @@ function legacyToolPartOutput(part) {
1716
1867
  }
1717
1868
  return state?.output ?? state?.result ?? state?.structured ?? state?.content ?? null;
1718
1869
  }
1719
- function isRecord(value) {
1870
+ function isRecord2(value) {
1720
1871
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1721
1872
  }
1722
1873
  function parseToolInput(state, props) {
@@ -1912,7 +2063,7 @@ async function emitContextFallback({
1912
2063
  emit({
1913
2064
  type: "finish-step",
1914
2065
  finishReason: {
1915
- unified: mapFinishReason(rawFinish),
2066
+ unified: mapOpenCodeFinishReason(rawFinish),
1916
2067
  raw: rawFinish
1917
2068
  },
1918
2069
  usage: mapUsage(assistant.tokens),
@@ -1989,16 +2140,6 @@ function emitAssistantContentPart(part, emit) {
1989
2140
  if (text) emit({ type: "reasoning-delta", id, delta: text });
1990
2141
  emit({ type: "reasoning-end", id });
1991
2142
  }
1992
- function mapFinishReason(reason) {
1993
- const normalized = reason.toLowerCase();
1994
- if (normalized.includes("length")) return "length";
1995
- if (normalized.includes("filter")) return "content-filter";
1996
- if (normalized.includes("tool")) return "tool-calls";
1997
- if (normalized.includes("error") || normalized.includes("fail"))
1998
- return "error";
1999
- if (normalized === "stop" || normalized === "end") return "stop";
2000
- return "other";
2001
- }
2002
2143
  async function startToolRelay({
2003
2144
  allowedScriptPaths,
2004
2145
  tools,
@@ -2123,13 +2264,13 @@ function modelRefFromAssistantSnapshot(assistant) {
2123
2264
  if (model) return model;
2124
2265
  const direct = modelRefFromValue(assistant);
2125
2266
  if (direct) return direct;
2126
- if (isRecord(assistant.metadata)) {
2267
+ if (isRecord2(assistant.metadata)) {
2127
2268
  return modelRefFromValue(assistant.metadata.assistant);
2128
2269
  }
2129
2270
  return void 0;
2130
2271
  }
2131
2272
  function modelRefFromSessionInfo(data) {
2132
- if (!isRecord(data)) return void 0;
2273
+ if (!isRecord2(data)) return void 0;
2133
2274
  return modelRefFromValue(data.model) ?? modelRefFromValue(data);
2134
2275
  }
2135
2276
  function modelRefFromStart(start) {
@@ -2141,7 +2282,7 @@ function modelRefFromStart(start) {
2141
2282
  };
2142
2283
  }
2143
2284
  function modelRefFromValue(value) {
2144
- if (!isRecord(value)) return void 0;
2285
+ if (!isRecord2(value)) return void 0;
2145
2286
  const providerID = stringValue(value.providerID);
2146
2287
  const modelID = stringValue(value.modelID ?? value.id);
2147
2288
  if (!providerID || !modelID) return void 0;
@@ -2180,14 +2321,8 @@ function formatError(error) {
2180
2321
  return String(error);
2181
2322
  }
2182
2323
  }
2183
- function serialiseError2(err) {
2184
- if (err instanceof Error) {
2185
- return { name: err.name, message: err.message, stack: err.stack };
2186
- }
2187
- return err;
2188
- }
2189
2324
  function emitFatal(message) {
2190
- process.stderr.write(`[opencode bridge] ${message}
2325
+ process.stderr.write(`[OpenCode bridge] ${message}
2191
2326
  `);
2192
2327
  process.exit(1);
2193
2328
  }