@ai-sdk/harness-opencode 1.0.78 → 1.0.80

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,21 @@
1
1
  # @ai-sdk/harness-opencode
2
2
 
3
+ ## 1.0.80
4
+
5
+ ### Patch Changes
6
+
7
+ - @ai-sdk/harness@1.0.79
8
+
9
+ ## 1.0.79
10
+
11
+ ### Patch Changes
12
+
13
+ - eace6fb: feat(harness): add experimental support for steering agent conversations mid-turn
14
+ - c0595b4: feat(harness): support passing a filesystem and process restricted sandbox session to `HarnessAgentSession`, using fallbacks in favor of the preferred network sandbox session methods
15
+ - Updated dependencies [eace6fb]
16
+ - Updated dependencies [c0595b4]
17
+ - @ai-sdk/harness@1.0.78
18
+
3
19
  ## 1.0.78
4
20
 
5
21
  ### Patch Changes
@@ -1,6 +1,7 @@
1
1
  // ../harness/dist/bridge/index.js
2
2
  import { appendFile, mkdir, writeFile } from "fs/promises";
3
3
  import { existsSync, readFileSync } from "fs";
4
+ import { randomUUID } from "crypto";
4
5
  import { env as procEnv, pid, stdout } from "process";
5
6
  import { WebSocketServer } from "ws";
6
7
  var DEBUG_LEVEL_WEIGHT = {
@@ -31,6 +32,106 @@ function formatBridgeError(err) {
31
32
  }
32
33
  return { message: String(err) };
33
34
  }
35
+ function createBridgeUserMessageQueue(options) {
36
+ const messages = [];
37
+ const waiters = [];
38
+ const entries = /* @__PURE__ */ new Map();
39
+ let closed = false;
40
+ let pendingCount = 0;
41
+ const enqueue = (input) => {
42
+ const existing = entries.get(input.messageId);
43
+ if (existing != null) {
44
+ if (existing.response != null) {
45
+ options.respond(existing.response);
46
+ }
47
+ return;
48
+ }
49
+ let settled = false;
50
+ const settle = (response) => {
51
+ if (settled) return;
52
+ settled = true;
53
+ pendingCount--;
54
+ const entry = entries.get(input.messageId);
55
+ if (entry != null) entry.response = response;
56
+ options.respond(response);
57
+ };
58
+ const message = {
59
+ messageId: input.messageId,
60
+ text: input.text,
61
+ accept: () => {
62
+ settle({
63
+ type: "user-message-response",
64
+ messageId: input.messageId,
65
+ accepted: true
66
+ });
67
+ },
68
+ reject: (error) => {
69
+ settle({
70
+ type: "user-message-response",
71
+ messageId: input.messageId,
72
+ accepted: false,
73
+ error: { message: formatBridgeError(error).message }
74
+ });
75
+ }
76
+ };
77
+ entries.set(input.messageId, {
78
+ reject: message.reject
79
+ });
80
+ pendingCount++;
81
+ if (closed) {
82
+ message.reject(
83
+ new Error("The bridge turn is no longer accepting user messages.")
84
+ );
85
+ return;
86
+ }
87
+ const waiter = waiters.shift();
88
+ if (waiter != null) {
89
+ waiter({ done: false, value: message });
90
+ } else {
91
+ messages.push(message);
92
+ }
93
+ };
94
+ const close = (error) => {
95
+ if (closed) return;
96
+ closed = true;
97
+ const reason = error ?? new Error("The bridge turn ended before accepting the user message.");
98
+ for (const entry of entries.values()) {
99
+ if (entry.response == null) entry.reject(reason);
100
+ }
101
+ messages.length = 0;
102
+ while (waiters.length > 0) {
103
+ waiters.shift()({ done: true, value: void 0 });
104
+ }
105
+ };
106
+ return {
107
+ get pendingCount() {
108
+ return pendingCount;
109
+ },
110
+ enqueue,
111
+ close,
112
+ [Symbol.asyncIterator]() {
113
+ return {
114
+ next: () => {
115
+ const message = messages.shift();
116
+ if (message != null) {
117
+ return Promise.resolve({ done: false, value: message });
118
+ }
119
+ if (closed) {
120
+ return Promise.resolve({
121
+ done: true,
122
+ value: void 0
123
+ });
124
+ }
125
+ return new Promise(
126
+ (resolve) => {
127
+ waiters.push(resolve);
128
+ }
129
+ );
130
+ }
131
+ };
132
+ }
133
+ };
134
+ }
34
135
  function parseEnvList(value) {
35
136
  if (!value) return void 0;
36
137
  const items = value.split(",").map((item) => item.trim()).filter(Boolean);
@@ -247,6 +348,9 @@ async function runBridge(options) {
247
348
  switch (msg.type) {
248
349
  case "start": {
249
350
  activeSocket = ws;
351
+ currentUserMessages?.close(
352
+ new Error("A new bridge turn replaced the active turn.")
353
+ );
250
354
  const firstTurn = isFirstTurn;
251
355
  isFirstTurn = false;
252
356
  eventLog = [];
@@ -266,6 +370,7 @@ async function runBridge(options) {
266
370
  if (debugConfig.enabled) {
267
371
  installConsoleCapture();
268
372
  }
373
+ const userMessages = createBridgeUserMessageQueue({ respond: emit });
269
374
  const turn = {
270
375
  emit,
271
376
  requestToolResult: (toolCallId) => new Promise((resolve) => {
@@ -274,7 +379,7 @@ async function runBridge(options) {
274
379
  requestToolApproval: (approvalId) => new Promise((resolve) => {
275
380
  pendingToolApprovals.set(approvalId, resolve);
276
381
  }),
277
- pendingUserMessages: [],
382
+ experimental_userMessages: userMessages,
278
383
  abortSignal: turnAbort.signal,
279
384
  firstTurn,
280
385
  bridgeLog: (input) => {
@@ -292,12 +397,16 @@ async function runBridge(options) {
292
397
  emitWarning,
293
398
  emitError
294
399
  };
295
- currentUserMessages = turn.pendingUserMessages;
400
+ currentUserMessages = userMessages;
296
401
  try {
297
402
  await onStart(msg, turn);
298
403
  } catch (err) {
299
404
  emitError({ error: err, message: "bridge turn failed" });
300
405
  } finally {
406
+ userMessages.close();
407
+ if (currentUserMessages === userMessages) {
408
+ currentUserMessages = void 0;
409
+ }
301
410
  currentTurnState = "waiting";
302
411
  void writeBridgeMeta("waiting");
303
412
  }
@@ -319,9 +428,34 @@ async function runBridge(options) {
319
428
  }
320
429
  return;
321
430
  }
322
- case "user-message":
323
- currentUserMessages?.push(msg.text);
431
+ case "user-message": {
432
+ const messageId = msg.messageId ?? randomUUID();
433
+ if (currentUserMessages == null) {
434
+ sendControl(ws, {
435
+ type: "user-message-response",
436
+ messageId,
437
+ accepted: false,
438
+ error: { message: "The bridge has no active turn to steer." }
439
+ });
440
+ return;
441
+ }
442
+ if (ws !== activeSocket) {
443
+ sendControl(ws, {
444
+ type: "user-message-response",
445
+ messageId,
446
+ accepted: false,
447
+ error: {
448
+ message: "The connection does not own the active bridge turn."
449
+ }
450
+ });
451
+ return;
452
+ }
453
+ currentUserMessages.enqueue({
454
+ messageId,
455
+ text: msg.text
456
+ });
324
457
  return;
458
+ }
325
459
  case "abort":
326
460
  turnAbort?.abort();
327
461
  return;
@@ -395,7 +529,8 @@ async function runBridge(options) {
395
529
  sendControl(ws, {
396
530
  type: "bridge-hello",
397
531
  state: currentTurnState,
398
- lastSeq: seqCounter
532
+ lastSeq: seqCounter,
533
+ capabilities: { experimental_userMessageResponses: true }
399
534
  });
400
535
  ws.on("message", (raw) => {
401
536
  let parsed;
@@ -456,7 +591,7 @@ function serialiseError(err) {
456
591
  }
457
592
 
458
593
  // src/bridge/index.ts
459
- import { randomUUID } from "crypto";
594
+ import { randomUUID as randomUUID2 } from "crypto";
460
595
  import { mkdirSync } from "fs";
461
596
  import path2 from "path";
462
597
  import { argv, env as procEnv2 } from "process";
@@ -536,9 +671,6 @@ function getOpenCodeEventSessionId(event) {
536
671
  if (typeof partObject?.sessionID === "string") return partObject.sessionID;
537
672
  return void 0;
538
673
  }
539
- function isStepSettlementEvent(event) {
540
- return event.type === "session.next.step.ended" || event.type === "session.next.step.failed" || event.type === "session.error";
541
- }
542
674
  function emitMissingFinalDelta({
543
675
  id,
544
676
  fullText,
@@ -1545,6 +1677,7 @@ async function runTurn(start, turn) {
1545
1677
  } catch (err) {
1546
1678
  turn.emitError({ error: err, message: "OpenCode turn failed" });
1547
1679
  } finally {
1680
+ turn.experimental_userMessages.close();
1548
1681
  emit({
1549
1682
  type: "finish",
1550
1683
  finishReason: { unified: "stop", raw: "stop" },
@@ -1741,11 +1874,12 @@ async function legacySessionCreate({
1741
1874
  async function legacySessionPrompt({
1742
1875
  client,
1743
1876
  sessionId,
1744
- start
1877
+ start,
1878
+ prompt: promptText
1745
1879
  }) {
1746
1880
  const session = client.session;
1747
- const prompt = session.promptAsync ?? session.prompt;
1748
- return prompt.call(session, {
1881
+ const submitPrompt = session.promptAsync ?? session.prompt;
1882
+ return submitPrompt.call(session, {
1749
1883
  sessionID: sessionId,
1750
1884
  ...start.instructions ? { system: start.instructions } : {},
1751
1885
  ...start.variant ? { variant: start.variant } : {},
@@ -1755,7 +1889,7 @@ async function legacySessionPrompt({
1755
1889
  schema: start.responseFormat.schema
1756
1890
  }
1757
1891
  } : {},
1758
- parts: [{ type: "text", text: start.prompt }]
1892
+ parts: [{ type: "text", text: promptText ?? start.prompt }]
1759
1893
  });
1760
1894
  }
1761
1895
  async function legacySessionSummarize({
@@ -1856,7 +1990,9 @@ async function runPrompt({
1856
1990
  let sawContent = false;
1857
1991
  let sawFinishStep = false;
1858
1992
  let sawBusy = false;
1993
+ let sawStructuredOutput = false;
1859
1994
  let terminalError;
1995
+ let submittingUserMessage = false;
1860
1996
  const state = createTranslationState();
1861
1997
  const initialSessionTokens = await readSessionTokens({
1862
1998
  client,
@@ -1899,7 +2035,7 @@ async function runPrompt({
1899
2035
  });
1900
2036
  const info = asOpenCodeObject(event.properties?.info);
1901
2037
  if (start.responseFormat?.type === "json" && info?.structured !== void 0) {
1902
- const id = String(info.id ?? randomUUID());
2038
+ const id = String(info.id ?? randomUUID2());
1903
2039
  emit({ type: "text-start", id });
1904
2040
  emit({
1905
2041
  type: "text-delta",
@@ -1913,20 +2049,23 @@ async function runPrompt({
1913
2049
  usage: defaultUsage()
1914
2050
  });
1915
2051
  sawFinishStep = true;
1916
- turnSettled.resolve("event");
1917
- return true;
2052
+ sawStructuredOutput = true;
2053
+ if (!submittingUserMessage && turn.experimental_userMessages.pendingCount === 0) {
2054
+ turn.experimental_userMessages.close();
2055
+ turnSettled.resolve("event");
2056
+ return true;
2057
+ }
1918
2058
  }
1919
2059
  }
1920
2060
  if (event.type === "session.updated") {
1921
2061
  latestSessionTokens = extractSessionTokens(event.properties) ?? latestSessionTokens;
1922
2062
  }
1923
- if (isStepSettlementEvent(event)) {
2063
+ if (event.type === "session.next.step.failed" || event.type === "session.error") {
2064
+ const error = formatError(event.properties?.error ?? event);
1924
2065
  if (event.type === "session.error") {
1925
- terminalError = formatError(event.properties?.error ?? event);
1926
- }
1927
- if (start.responseFormat?.type === "json" && event.type === "session.next.step.ended") {
1928
- return;
2066
+ terminalError = error;
1929
2067
  }
2068
+ turn.experimental_userMessages.close(new Error(error));
1930
2069
  turnSettled.resolve("event");
1931
2070
  return true;
1932
2071
  }
@@ -1937,7 +2076,9 @@ async function runPrompt({
1937
2076
  sawBusy = true;
1938
2077
  turn.emitWarning({ message: legacyRetryStatusMessage(event) });
1939
2078
  } else if (sawBusy && status === "idle") {
1940
- if (start.responseFormat?.type !== "json") {
2079
+ sawBusy = false;
2080
+ if (!submittingUserMessage && turn.experimental_userMessages.pendingCount === 0 && (start.responseFormat?.type !== "json" || sawStructuredOutput)) {
2081
+ turn.experimental_userMessages.close();
1941
2082
  turnSettled.resolve("event");
1942
2083
  return true;
1943
2084
  }
@@ -1945,9 +2086,36 @@ async function runPrompt({
1945
2086
  }
1946
2087
  }).finally(() => {
1947
2088
  eventsReady.resolve(void 0);
2089
+ turn.experimental_userMessages.close(
2090
+ new Error("OpenCode event stream ended before the turn settled.")
2091
+ );
1948
2092
  turnSettled.resolve("stream-ended");
1949
2093
  });
1950
2094
  await eventsReady.promise;
2095
+ const userMessageLoop = (async () => {
2096
+ for await (const message of turn.experimental_userMessages) {
2097
+ submittingUserMessage = true;
2098
+ try {
2099
+ const prompted2 = await legacySessionPrompt({
2100
+ client,
2101
+ sessionId,
2102
+ start,
2103
+ prompt: message.text
2104
+ });
2105
+ if (prompted2.error) {
2106
+ message.reject(
2107
+ new Error(`OpenCode prompt failed: ${formatError(prompted2.error)}`)
2108
+ );
2109
+ continue;
2110
+ }
2111
+ message.accept();
2112
+ } catch (error) {
2113
+ message.reject(error);
2114
+ } finally {
2115
+ submittingUserMessage = false;
2116
+ }
2117
+ }
2118
+ })();
1951
2119
  const prompted = await legacySessionPrompt({
1952
2120
  client,
1953
2121
  sessionId,
@@ -1955,12 +2123,17 @@ async function runPrompt({
1955
2123
  });
1956
2124
  if (prompted.error) {
1957
2125
  eventsAbort.abort();
2126
+ turn.experimental_userMessages.close(
2127
+ new Error(`OpenCode prompt failed: ${formatError(prompted.error)}`)
2128
+ );
1958
2129
  throw new Error(`OpenCode prompt failed: ${formatError(prompted.error)}`);
1959
2130
  }
1960
2131
  const settlement = await turnSettled.promise;
1961
2132
  eventsAbort.abort();
1962
2133
  await eventLoop.catch(() => {
1963
2134
  });
2135
+ await userMessageLoop.catch(() => {
2136
+ });
1964
2137
  if (settlement === "stream-ended") {
1965
2138
  throw new Error("OpenCode event stream ended before the turn settled.");
1966
2139
  }
@@ -2394,7 +2567,7 @@ function emitAssistantContentPart(part, emit) {
2394
2567
  if (!part || typeof part !== "object") return;
2395
2568
  const value = part;
2396
2569
  if (value.type !== "text" && value.type !== "reasoning") return;
2397
- const id = typeof value.id === "string" && value.id.length > 0 ? value.id : `${value.type}-${randomUUID()}`;
2570
+ const id = typeof value.id === "string" && value.id.length > 0 ? value.id : `${value.type}-${randomUUID2()}`;
2398
2571
  const text = typeof value.text === "string" ? value.text : "";
2399
2572
  if (value.type === "text") {
2400
2573
  emit({ type: "text-start", id });