@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 +18 -0
- package/dist/bridge/index.mjs +194 -59
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +42 -39
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge/index.ts +93 -35
- package/src/bridge/opencode-finish-step.ts +39 -0
- package/src/bridge/opencode-usage.ts +5 -1
- package/src/opencode-harness.ts +42 -41
package/src/bridge/index.ts
CHANGED
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
type OpenCodeEvent,
|
|
22
22
|
unwrapOpenCodeEvent,
|
|
23
23
|
} from './opencode-events';
|
|
24
|
+
import {
|
|
25
|
+
legacyStepFinishPartToFinishStep,
|
|
26
|
+
mapOpenCodeFinishReason,
|
|
27
|
+
} from './opencode-finish-step';
|
|
24
28
|
import { prependOpenCodeBinToPath } from './opencode-path';
|
|
25
29
|
import {
|
|
26
30
|
addUsage,
|
|
@@ -144,7 +148,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
144
148
|
totalUsage = await runPrompt({ client, sessionId, start, turn, emit });
|
|
145
149
|
}
|
|
146
150
|
} catch (err) {
|
|
147
|
-
|
|
151
|
+
turn.emitError({ error: err, message: 'OpenCode turn failed' });
|
|
148
152
|
} finally {
|
|
149
153
|
emit({
|
|
150
154
|
type: 'finish',
|
|
@@ -469,11 +473,45 @@ function legacyStatusType(event: OpenCodeEvent): string | undefined {
|
|
|
469
473
|
: undefined;
|
|
470
474
|
}
|
|
471
475
|
|
|
472
|
-
function
|
|
476
|
+
function legacyRetryStatusMessage(event: OpenCodeEvent): string {
|
|
473
477
|
const status = event.properties?.status;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
478
|
+
const details: string[] = [];
|
|
479
|
+
if (status && typeof status === 'object') {
|
|
480
|
+
const retryStatus = status as { attempt?: unknown; message?: unknown };
|
|
481
|
+
if (typeof retryStatus.attempt === 'number') {
|
|
482
|
+
details.push(`attempt ${retryStatus.attempt}`);
|
|
483
|
+
}
|
|
484
|
+
if (typeof retryStatus.message === 'string' && retryStatus.message.trim()) {
|
|
485
|
+
details.push(retryStatus.message.trim());
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return details.length > 0
|
|
489
|
+
? `OpenCode session retry: ${details.join('; ')}`
|
|
490
|
+
: 'OpenCode session retry';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function nextRetryEventMessage(event: OpenCodeEvent): string {
|
|
494
|
+
const props = event.properties ?? {};
|
|
495
|
+
const details: string[] = [];
|
|
496
|
+
if (typeof props.attempt === 'number') {
|
|
497
|
+
details.push(`attempt ${props.attempt}`);
|
|
498
|
+
}
|
|
499
|
+
const error = props.error;
|
|
500
|
+
if (isRecord(error)) {
|
|
501
|
+
const message =
|
|
502
|
+
stringValue(error.message) ??
|
|
503
|
+
(isRecord(error.data) ? stringValue(error.data.message) : undefined);
|
|
504
|
+
const statusCode = error.statusCode;
|
|
505
|
+
if (typeof statusCode === 'number') {
|
|
506
|
+
details.push(`HTTP ${statusCode}`);
|
|
507
|
+
}
|
|
508
|
+
if (message) details.push(message);
|
|
509
|
+
} else if (error != null) {
|
|
510
|
+
details.push(formatError(error));
|
|
511
|
+
}
|
|
512
|
+
return details.length > 0
|
|
513
|
+
? `OpenCode session retry: ${details.join('; ')}`
|
|
514
|
+
: 'OpenCode session retry';
|
|
477
515
|
}
|
|
478
516
|
|
|
479
517
|
async function ensureSession({
|
|
@@ -569,9 +607,7 @@ async function runPrompt({
|
|
|
569
607
|
sawBusy = true;
|
|
570
608
|
} else if (status === 'retry') {
|
|
571
609
|
sawBusy = true;
|
|
572
|
-
|
|
573
|
-
turnSettled.resolve();
|
|
574
|
-
return true;
|
|
610
|
+
turn.emitWarning({ message: legacyRetryStatusMessage(event) });
|
|
575
611
|
} else if (sawBusy && status === 'idle') {
|
|
576
612
|
turnSettled.resolve();
|
|
577
613
|
return true;
|
|
@@ -682,9 +718,7 @@ async function runCompaction({
|
|
|
682
718
|
sawBusy = true;
|
|
683
719
|
} else if (status === 'retry') {
|
|
684
720
|
sawBusy = true;
|
|
685
|
-
|
|
686
|
-
compactionSettled.resolve();
|
|
687
|
-
return true;
|
|
721
|
+
turn.emitWarning({ message: legacyRetryStatusMessage(event) });
|
|
688
722
|
} else if (sawBusy && status === 'idle') {
|
|
689
723
|
compactionSettled.resolve();
|
|
690
724
|
return true;
|
|
@@ -777,6 +811,7 @@ type TranslationState = {
|
|
|
777
811
|
turnUsage: Record<string, unknown> | undefined;
|
|
778
812
|
legacyTextPartIds: Set<string>;
|
|
779
813
|
legacyReasoningPartIds: Set<string>;
|
|
814
|
+
legacyStepFinishPartIds: Set<string>;
|
|
780
815
|
};
|
|
781
816
|
|
|
782
817
|
function createTranslationState(): TranslationState {
|
|
@@ -793,6 +828,7 @@ function createTranslationState(): TranslationState {
|
|
|
793
828
|
turnUsage: undefined,
|
|
794
829
|
legacyTextPartIds: new Set(),
|
|
795
830
|
legacyReasoningPartIds: new Set(),
|
|
831
|
+
legacyStepFinishPartIds: new Set(),
|
|
796
832
|
};
|
|
797
833
|
}
|
|
798
834
|
|
|
@@ -860,6 +896,7 @@ async function translateAndEmit({
|
|
|
860
896
|
|
|
861
897
|
if (type === 'message.part.updated') {
|
|
862
898
|
if (emitLegacyTextPartUpdate({ part: props.part, state, emit })) return;
|
|
899
|
+
if (emitLegacyStepFinishPart({ part: props.part, state, emit })) return;
|
|
863
900
|
emitLegacyToolPart({ part: props.part, state, emit });
|
|
864
901
|
return;
|
|
865
902
|
}
|
|
@@ -1020,13 +1057,25 @@ async function translateAndEmit({
|
|
|
1020
1057
|
});
|
|
1021
1058
|
return;
|
|
1022
1059
|
}
|
|
1060
|
+
if (type === 'session.next.retried') {
|
|
1061
|
+
const error = props.error ?? event;
|
|
1062
|
+
if (isRecord(error) && error.isRetryable === false) {
|
|
1063
|
+
turn.emitError({
|
|
1064
|
+
error,
|
|
1065
|
+
message: 'OpenCode session retry failed',
|
|
1066
|
+
});
|
|
1067
|
+
} else {
|
|
1068
|
+
turn.emitWarning({ message: nextRetryEventMessage(event) });
|
|
1069
|
+
}
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1023
1072
|
if (type === 'session.next.step.ended') {
|
|
1024
1073
|
closeLegacyOpenParts({ state, emit });
|
|
1025
1074
|
state.turnUsage = mapUsage(props.tokens);
|
|
1026
1075
|
emit({
|
|
1027
1076
|
type: 'finish-step',
|
|
1028
1077
|
finishReason: {
|
|
1029
|
-
unified:
|
|
1078
|
+
unified: mapOpenCodeFinishReason(String(props.finish ?? 'stop')),
|
|
1030
1079
|
raw: String(props.finish ?? 'stop'),
|
|
1031
1080
|
},
|
|
1032
1081
|
usage: state.turnUsage,
|
|
@@ -1058,7 +1107,14 @@ async function translateAndEmit({
|
|
|
1058
1107
|
return;
|
|
1059
1108
|
}
|
|
1060
1109
|
if (type === 'session.error' || type === 'session.next.step.failed') {
|
|
1061
|
-
|
|
1110
|
+
const error = props.error ?? event;
|
|
1111
|
+
turn.emitError({
|
|
1112
|
+
error,
|
|
1113
|
+
message:
|
|
1114
|
+
type === 'session.error'
|
|
1115
|
+
? 'OpenCode session error'
|
|
1116
|
+
: 'OpenCode step failed',
|
|
1117
|
+
});
|
|
1062
1118
|
return;
|
|
1063
1119
|
}
|
|
1064
1120
|
if (type === 'permission.v2.asked') {
|
|
@@ -1187,6 +1243,28 @@ function closeLegacyOpenParts({
|
|
|
1187
1243
|
state.legacyTextPartIds.clear();
|
|
1188
1244
|
}
|
|
1189
1245
|
|
|
1246
|
+
function emitLegacyStepFinishPart({
|
|
1247
|
+
part,
|
|
1248
|
+
state,
|
|
1249
|
+
emit,
|
|
1250
|
+
}: {
|
|
1251
|
+
part: unknown;
|
|
1252
|
+
state: TranslationState;
|
|
1253
|
+
emit: Emit;
|
|
1254
|
+
}): boolean {
|
|
1255
|
+
const event = legacyStepFinishPartToFinishStep(part);
|
|
1256
|
+
if (!event) return false;
|
|
1257
|
+
const id = isRecord(part) ? stringValue(part.id) : undefined;
|
|
1258
|
+
if (id) {
|
|
1259
|
+
if (state.legacyStepFinishPartIds.has(id)) return true;
|
|
1260
|
+
state.legacyStepFinishPartIds.add(id);
|
|
1261
|
+
}
|
|
1262
|
+
closeLegacyOpenParts({ state, emit });
|
|
1263
|
+
state.turnUsage = event.usage as Record<string, unknown>;
|
|
1264
|
+
emit(event);
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1190
1268
|
function emitLegacyToolPart({
|
|
1191
1269
|
part,
|
|
1192
1270
|
state,
|
|
@@ -1592,7 +1670,7 @@ async function emitContextFallback({
|
|
|
1592
1670
|
emit({
|
|
1593
1671
|
type: 'finish-step',
|
|
1594
1672
|
finishReason: {
|
|
1595
|
-
unified:
|
|
1673
|
+
unified: mapOpenCodeFinishReason(rawFinish),
|
|
1596
1674
|
raw: rawFinish,
|
|
1597
1675
|
},
|
|
1598
1676
|
usage: mapUsage(assistant.tokens),
|
|
@@ -1724,19 +1802,6 @@ function emitAssistantContentPart(part: unknown, emit: Emit): void {
|
|
|
1724
1802
|
emit({ type: 'reasoning-end', id });
|
|
1725
1803
|
}
|
|
1726
1804
|
|
|
1727
|
-
function mapFinishReason(
|
|
1728
|
-
reason: string,
|
|
1729
|
-
): 'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' {
|
|
1730
|
-
const normalized = reason.toLowerCase();
|
|
1731
|
-
if (normalized.includes('length')) return 'length';
|
|
1732
|
-
if (normalized.includes('filter')) return 'content-filter';
|
|
1733
|
-
if (normalized.includes('tool')) return 'tool-calls';
|
|
1734
|
-
if (normalized.includes('error') || normalized.includes('fail'))
|
|
1735
|
-
return 'error';
|
|
1736
|
-
if (normalized === 'stop' || normalized === 'end') return 'stop';
|
|
1737
|
-
return 'other';
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
1805
|
async function startToolRelay({
|
|
1741
1806
|
allowedScriptPaths,
|
|
1742
1807
|
tools,
|
|
@@ -1980,14 +2045,7 @@ function formatError(error: unknown): string {
|
|
|
1980
2045
|
}
|
|
1981
2046
|
}
|
|
1982
2047
|
|
|
1983
|
-
function serialiseError(err: unknown): unknown {
|
|
1984
|
-
if (err instanceof Error) {
|
|
1985
|
-
return { name: err.name, message: err.message, stack: err.stack };
|
|
1986
|
-
}
|
|
1987
|
-
return err;
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
2048
|
function emitFatal(message: string): never {
|
|
1991
|
-
process.stderr.write(`[
|
|
2049
|
+
process.stderr.write(`[OpenCode bridge] ${message}\n`);
|
|
1992
2050
|
process.exit(1);
|
|
1993
2051
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HarnessV1StreamPart } from '@ai-sdk/harness';
|
|
2
|
+
import { mapUsage } from './opencode-usage';
|
|
3
|
+
|
|
4
|
+
type FinishStepEvent = Extract<HarnessV1StreamPart, { type: 'finish-step' }>;
|
|
5
|
+
|
|
6
|
+
export function mapOpenCodeFinishReason(
|
|
7
|
+
reason: string,
|
|
8
|
+
): 'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' {
|
|
9
|
+
const normalized = reason.toLowerCase();
|
|
10
|
+
if (normalized.includes('length')) return 'length';
|
|
11
|
+
if (normalized.includes('filter')) return 'content-filter';
|
|
12
|
+
if (normalized.includes('tool')) return 'tool-calls';
|
|
13
|
+
if (normalized.includes('error') || normalized.includes('fail'))
|
|
14
|
+
return 'error';
|
|
15
|
+
if (normalized === 'stop' || normalized === 'end') return 'stop';
|
|
16
|
+
return 'other';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function legacyStepFinishPartToFinishStep(
|
|
20
|
+
part: unknown,
|
|
21
|
+
): FinishStepEvent | undefined {
|
|
22
|
+
if (!isRecord(part) || part.type !== 'step-finish') return undefined;
|
|
23
|
+
const rawFinish = typeof part.reason === 'string' ? part.reason : 'stop';
|
|
24
|
+
return {
|
|
25
|
+
type: 'finish-step',
|
|
26
|
+
finishReason: {
|
|
27
|
+
unified: mapOpenCodeFinishReason(rawFinish),
|
|
28
|
+
raw: rawFinish,
|
|
29
|
+
},
|
|
30
|
+
usage: mapUsage(part.tokens),
|
|
31
|
+
...(typeof part.cost === 'number'
|
|
32
|
+
? { harnessMetadata: { opencode: { cost: part.cost } } }
|
|
33
|
+
: {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
38
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
39
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { HarnessV1StreamPart } from '@ai-sdk/harness';
|
|
2
|
+
|
|
3
|
+
type FinishStepEvent = Extract<HarnessV1StreamPart, { type: 'finish-step' }>;
|
|
4
|
+
|
|
1
5
|
export type OpenCodeTokenUsage = {
|
|
2
6
|
readonly input: number;
|
|
3
7
|
readonly output: number;
|
|
@@ -8,7 +12,7 @@ export type OpenCodeTokenUsage = {
|
|
|
8
12
|
};
|
|
9
13
|
};
|
|
10
14
|
|
|
11
|
-
export type HarnessUsage =
|
|
15
|
+
export type HarnessUsage = FinishStepEvent['usage'];
|
|
12
16
|
|
|
13
17
|
export function mapUsage(tokens: unknown): HarnessUsage {
|
|
14
18
|
const value = extractOpenCodeTokens(tokens) ?? zeroOpenCodeTokens();
|
package/src/opencode-harness.ts
CHANGED
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
} from '@ai-sdk/harness';
|
|
24
24
|
import {
|
|
25
25
|
classifyDiskLog,
|
|
26
|
+
createBridgeErrorHandler,
|
|
27
|
+
createBridgeStartupError,
|
|
28
|
+
drainBridgeProcessStream,
|
|
29
|
+
forwardBridgeProcessStream,
|
|
26
30
|
markBridgeStarting,
|
|
27
31
|
resolveSandboxHomeDir,
|
|
28
32
|
SandboxChannel,
|
|
@@ -274,6 +278,10 @@ export function createOpenCode(
|
|
|
274
278
|
}),
|
|
275
279
|
)
|
|
276
280
|
: undefined;
|
|
281
|
+
const onBridgeError = createBridgeErrorHandler({
|
|
282
|
+
harnessId: 'opencode',
|
|
283
|
+
sessionId: startOpts.sessionId,
|
|
284
|
+
});
|
|
277
285
|
|
|
278
286
|
if (coords) {
|
|
279
287
|
try {
|
|
@@ -287,6 +295,7 @@ export function createOpenCode(
|
|
|
287
295
|
outboundSchema: outboundMessageSchema,
|
|
288
296
|
initialLastSeenEventId: coords.lastSeenEventId,
|
|
289
297
|
onDiagnostic,
|
|
298
|
+
onBridgeError,
|
|
290
299
|
});
|
|
291
300
|
await attachChannel.open(isContinue ? { resume: true } : undefined);
|
|
292
301
|
return createSession({
|
|
@@ -383,6 +392,13 @@ export function createOpenCode(
|
|
|
383
392
|
env,
|
|
384
393
|
abortSignal: startOpts.abortSignal,
|
|
385
394
|
});
|
|
395
|
+
const stderrTail: string[] = [];
|
|
396
|
+
const bridgeStderrDone = forwardBridgeProcessStream({
|
|
397
|
+
stream: proc.stderr,
|
|
398
|
+
streamName: 'stderr',
|
|
399
|
+
source: 'opencode',
|
|
400
|
+
collectTail: stderrTail,
|
|
401
|
+
});
|
|
386
402
|
|
|
387
403
|
const { port: boundPort } = await waitForBridgeReady({
|
|
388
404
|
proc,
|
|
@@ -391,13 +407,24 @@ export function createOpenCode(
|
|
|
391
407
|
bridgeType: 'opencode',
|
|
392
408
|
timeoutMs,
|
|
393
409
|
abortSignal: startOpts.abortSignal,
|
|
394
|
-
createTimeoutError: () =>
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
410
|
+
createTimeoutError: ({ proc, stdoutTail }) =>
|
|
411
|
+
createBridgeStartupError({
|
|
412
|
+
message: 'OpenCode bridge did not become ready in time.',
|
|
413
|
+
proc,
|
|
414
|
+
stdoutTail,
|
|
415
|
+
stderrTail,
|
|
416
|
+
stderrDone: bridgeStderrDone,
|
|
417
|
+
}),
|
|
418
|
+
createExitError: ({ proc, stdoutTail }) =>
|
|
419
|
+
createBridgeStartupError({
|
|
420
|
+
message: 'OpenCode bridge exited before becoming ready.',
|
|
421
|
+
proc,
|
|
422
|
+
stdoutTail,
|
|
423
|
+
stderrTail,
|
|
424
|
+
stderrDone: bridgeStderrDone,
|
|
425
|
+
}),
|
|
398
426
|
});
|
|
399
|
-
void
|
|
400
|
-
void forwardBridgeStderr(proc.stderr);
|
|
427
|
+
void drainBridgeProcessStream(proc.stdout);
|
|
401
428
|
|
|
402
429
|
const wsUrl =
|
|
403
430
|
(await sandboxSession.getPortUrl({
|
|
@@ -409,6 +436,7 @@ export function createOpenCode(
|
|
|
409
436
|
connect: () => openWebSocket(wsUrl),
|
|
410
437
|
outboundSchema: outboundMessageSchema,
|
|
411
438
|
onDiagnostic,
|
|
439
|
+
onBridgeError,
|
|
412
440
|
...(respawnStrategy === 'replay'
|
|
413
441
|
? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 }
|
|
414
442
|
: {}),
|
|
@@ -448,7 +476,7 @@ function resolveBridgePort(
|
|
|
448
476
|
throw new HarnessCapabilityUnsupportedError({
|
|
449
477
|
harnessId: 'opencode',
|
|
450
478
|
message:
|
|
451
|
-
'The
|
|
479
|
+
'The OpenCode harness needs a TCP port exposed by the sandbox. ' +
|
|
452
480
|
'Create the sandbox with `ports: [<port>]` or pass `createOpenCode({ port })`.',
|
|
453
481
|
});
|
|
454
482
|
}
|
|
@@ -497,34 +525,6 @@ async function writeOpenCodeSkills({
|
|
|
497
525
|
return { skillsDir };
|
|
498
526
|
}
|
|
499
527
|
|
|
500
|
-
async function forwardBridgeStderr(
|
|
501
|
-
stream: ReadableStream<Uint8Array>,
|
|
502
|
-
): Promise<void> {
|
|
503
|
-
try {
|
|
504
|
-
const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
|
|
505
|
-
while (true) {
|
|
506
|
-
const { value, done } = await reader.read();
|
|
507
|
-
if (done) return;
|
|
508
|
-
if (value) {
|
|
509
|
-
const trimmed = value.endsWith('\n') ? value.slice(0, -1) : value;
|
|
510
|
-
if (trimmed.length > 0) {
|
|
511
|
-
console.log(`[bridge stderr] ${trimmed}`);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
} catch {}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
async function drainRest(stream: ReadableStream<Uint8Array>): Promise<void> {
|
|
519
|
-
try {
|
|
520
|
-
const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
|
|
521
|
-
while (true) {
|
|
522
|
-
const { done } = await reader.read();
|
|
523
|
-
if (done) return;
|
|
524
|
-
}
|
|
525
|
-
} catch {}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
528
|
function openWebSocket(url: string): Promise<WebSocket> {
|
|
529
529
|
return new Promise((resolve, reject) => {
|
|
530
530
|
const ws = new WebSocket(url);
|
|
@@ -668,7 +668,7 @@ function createSession({
|
|
|
668
668
|
return;
|
|
669
669
|
}
|
|
670
670
|
settleError(
|
|
671
|
-
new Error('
|
|
671
|
+
new Error('OpenCode bridge closed before the turn finished.'),
|
|
672
672
|
);
|
|
673
673
|
};
|
|
674
674
|
channel.onClose(onClose);
|
|
@@ -811,7 +811,7 @@ function createSession({
|
|
|
811
811
|
doDetach: async () => {
|
|
812
812
|
if (stopped) {
|
|
813
813
|
throw new Error(
|
|
814
|
-
`
|
|
814
|
+
`OpenCode session ${sessionId} is already stopped; cannot detach.`,
|
|
815
815
|
);
|
|
816
816
|
}
|
|
817
817
|
stopped = true;
|
|
@@ -867,7 +867,7 @@ function createSession({
|
|
|
867
867
|
doStop: async () => {
|
|
868
868
|
if (stopped) {
|
|
869
869
|
throw new Error(
|
|
870
|
-
`
|
|
870
|
+
`OpenCode session ${sessionId} is already stopped; cannot stop.`,
|
|
871
871
|
);
|
|
872
872
|
}
|
|
873
873
|
stopped = true;
|
|
@@ -879,7 +879,7 @@ function createSession({
|
|
|
879
879
|
unsub();
|
|
880
880
|
reject(
|
|
881
881
|
new Error(
|
|
882
|
-
`
|
|
882
|
+
`OpenCode session ${sessionId} did not reply to detach within 5s.`,
|
|
883
883
|
),
|
|
884
884
|
);
|
|
885
885
|
}, 5000);
|
|
@@ -928,10 +928,11 @@ function createSession({
|
|
|
928
928
|
doSuspendTurn: async () => {
|
|
929
929
|
if (stopped) {
|
|
930
930
|
throw new Error(
|
|
931
|
-
`
|
|
931
|
+
`OpenCode session ${sessionId} is stopped; cannot suspend.`,
|
|
932
932
|
);
|
|
933
933
|
}
|
|
934
934
|
stopped = true;
|
|
935
|
+
await channel.interrupt();
|
|
935
936
|
const lastSeenEventId = await channel.suspend();
|
|
936
937
|
const payload: HarnessV1ContinueTurnState = {
|
|
937
938
|
type: 'continue-turn',
|
|
@@ -1011,7 +1012,7 @@ function extractUserText(prompt: HarnessV1Prompt): string {
|
|
|
1011
1012
|
if (part.type !== 'text') {
|
|
1012
1013
|
throw new HarnessCapabilityUnsupportedError({
|
|
1013
1014
|
harnessId: 'opencode',
|
|
1014
|
-
message: `The
|
|
1015
|
+
message: `The OpenCode harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`,
|
|
1015
1016
|
});
|
|
1016
1017
|
}
|
|
1017
1018
|
parts.push(part.text);
|