@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 +16 -0
- package/dist/bridge/index.mjs +196 -23
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +192 -41
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge/index.ts +62 -15
- package/src/opencode-harness.ts +242 -45
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
|
package/dist/bridge/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1748
|
-
return
|
|
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 ??
|
|
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
|
-
|
|
1917
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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}-${
|
|
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 });
|