@ai-sdk/harness-opencode 1.0.101 → 1.0.103
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 +154 -13
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +683 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +7 -3
- package/src/bridge/index.ts +92 -3
- package/src/bridge/question-tool.ts +90 -0
- package/src/opencode-harness.ts +17 -2
package/src/bridge/index.ts
CHANGED
|
@@ -44,6 +44,12 @@ import {
|
|
|
44
44
|
type OpenCodeObject,
|
|
45
45
|
} from './opencode-types';
|
|
46
46
|
import { startAuthorizedToolRelay, type ToolRelay } from './tool-relay';
|
|
47
|
+
import {
|
|
48
|
+
openCodeQuestionKey,
|
|
49
|
+
toHarnessQuestionsInput,
|
|
50
|
+
toOpenCodeQuestionResponse,
|
|
51
|
+
type OpenCodeQuestionRequest,
|
|
52
|
+
} from './question-tool';
|
|
47
53
|
|
|
48
54
|
type Emit = (msg: Record<string, unknown>) => void;
|
|
49
55
|
|
|
@@ -65,7 +71,8 @@ type CommonBuiltinToolName =
|
|
|
65
71
|
| 'edit'
|
|
66
72
|
| 'bash'
|
|
67
73
|
| 'glob'
|
|
68
|
-
| 'grep'
|
|
74
|
+
| 'grep'
|
|
75
|
+
| 'askUserQuestions';
|
|
69
76
|
|
|
70
77
|
const NATIVE_TO_COMMON: Readonly<Record<string, CommonBuiltinToolName>> = {
|
|
71
78
|
view: 'read',
|
|
@@ -75,6 +82,7 @@ const NATIVE_TO_COMMON: Readonly<Record<string, CommonBuiltinToolName>> = {
|
|
|
75
82
|
bash: 'bash',
|
|
76
83
|
glob: 'glob',
|
|
77
84
|
grep: 'grep',
|
|
85
|
+
question: 'askUserQuestions',
|
|
78
86
|
};
|
|
79
87
|
|
|
80
88
|
const OPENCODE_TO_WIRE: Readonly<Record<string, string>> = {
|
|
@@ -83,6 +91,7 @@ const OPENCODE_TO_WIRE: Readonly<Record<string, string>> = {
|
|
|
83
91
|
webfetch: 'webfetch',
|
|
84
92
|
task: 'agent',
|
|
85
93
|
agent: 'agent',
|
|
94
|
+
askUserQuestions: 'question',
|
|
86
95
|
subtask: 'agent',
|
|
87
96
|
};
|
|
88
97
|
|
|
@@ -256,7 +265,7 @@ function buildOpenCodeConfig({
|
|
|
256
265
|
webfetch: 'ask',
|
|
257
266
|
doom_loop: 'ask',
|
|
258
267
|
task: 'ask',
|
|
259
|
-
question: '
|
|
268
|
+
question: 'allow',
|
|
260
269
|
},
|
|
261
270
|
};
|
|
262
271
|
if (start.model) config.model = start.model;
|
|
@@ -1035,7 +1044,14 @@ async function consumeEvents({
|
|
|
1035
1044
|
: undefined;
|
|
1036
1045
|
if (!scopedSessionId) continue;
|
|
1037
1046
|
const isDescendant = scopedSessionId !== sessionId;
|
|
1038
|
-
if (event.type === '
|
|
1047
|
+
if (event.type === 'question.asked') {
|
|
1048
|
+
await handleQuestion({
|
|
1049
|
+
client,
|
|
1050
|
+
turn,
|
|
1051
|
+
emit,
|
|
1052
|
+
event,
|
|
1053
|
+
});
|
|
1054
|
+
} else if (event.type === 'permission.v2.asked') {
|
|
1039
1055
|
await handlePermissionV2({
|
|
1040
1056
|
client,
|
|
1041
1057
|
sessionId: scopedSessionId,
|
|
@@ -1075,6 +1091,79 @@ function getSubagentStepId(event: OpenCodeEvent | undefined) {
|
|
|
1075
1091
|
return stringValue(event.properties?.stepID) ?? event.id;
|
|
1076
1092
|
}
|
|
1077
1093
|
|
|
1094
|
+
async function handleQuestion({
|
|
1095
|
+
client,
|
|
1096
|
+
turn,
|
|
1097
|
+
emit,
|
|
1098
|
+
event,
|
|
1099
|
+
}: {
|
|
1100
|
+
client: OpenCodeClient;
|
|
1101
|
+
turn: BridgeTurn;
|
|
1102
|
+
emit: Emit;
|
|
1103
|
+
event: OpenCodeEvent;
|
|
1104
|
+
}): Promise<void> {
|
|
1105
|
+
const nativeRequest = event.properties as OpenCodeQuestionRequest | undefined;
|
|
1106
|
+
if (
|
|
1107
|
+
nativeRequest == null ||
|
|
1108
|
+
typeof nativeRequest.id !== 'string' ||
|
|
1109
|
+
typeof nativeRequest.sessionID !== 'string' ||
|
|
1110
|
+
!Array.isArray(nativeRequest.questions)
|
|
1111
|
+
) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const toolCallId = nativeRequest.tool?.callID ?? nativeRequest.id;
|
|
1115
|
+
|
|
1116
|
+
emit({
|
|
1117
|
+
type: 'tool-call',
|
|
1118
|
+
toolCallId,
|
|
1119
|
+
toolName: 'askUserQuestions',
|
|
1120
|
+
nativeName: 'question',
|
|
1121
|
+
input: JSON.stringify(toHarnessQuestionsInput(nativeRequest)),
|
|
1122
|
+
providerExecuted: false,
|
|
1123
|
+
providerMetadata: {
|
|
1124
|
+
opencode: {
|
|
1125
|
+
nativeRequest,
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
const questionKey = openCodeQuestionKey(nativeRequest);
|
|
1131
|
+
const result = await turn.requestToolResult({
|
|
1132
|
+
toolCallId,
|
|
1133
|
+
matches: candidate => {
|
|
1134
|
+
const continuedRequest = candidate.toolResult?.providerOptions?.opencode
|
|
1135
|
+
?.nativeRequest as OpenCodeQuestionRequest | undefined;
|
|
1136
|
+
return (
|
|
1137
|
+
continuedRequest != null &&
|
|
1138
|
+
openCodeQuestionKey(continuedRequest) === questionKey
|
|
1139
|
+
);
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
const nativeResponse = toOpenCodeQuestionResponse({
|
|
1143
|
+
nativeRequest,
|
|
1144
|
+
output: result.output as Parameters<
|
|
1145
|
+
typeof toOpenCodeQuestionResponse
|
|
1146
|
+
>[0]['output'],
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
const response =
|
|
1150
|
+
nativeResponse.action === 'reject'
|
|
1151
|
+
? await client.question.reject({
|
|
1152
|
+
requestID: nativeRequest.id,
|
|
1153
|
+
directory: workdir,
|
|
1154
|
+
})
|
|
1155
|
+
: await client.question.reply({
|
|
1156
|
+
requestID: nativeRequest.id,
|
|
1157
|
+
directory: workdir,
|
|
1158
|
+
answers: nativeResponse.answers,
|
|
1159
|
+
});
|
|
1160
|
+
if (response.error != null) {
|
|
1161
|
+
throw new Error(
|
|
1162
|
+
`OpenCode question response failed: ${formatError(response.error)}`,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1078
1167
|
function sanitizeMcpToolName(value: string): string {
|
|
1079
1168
|
return value.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
1080
1169
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HarnessV1QuestionsToolInput,
|
|
3
|
+
HarnessV1QuestionsToolOutput,
|
|
4
|
+
} from '@ai-sdk/harness';
|
|
5
|
+
import type { QuestionInfo } from '@opencode-ai/sdk/v2';
|
|
6
|
+
|
|
7
|
+
export type OpenCodeQuestionRequest = {
|
|
8
|
+
id: string;
|
|
9
|
+
sessionID: string;
|
|
10
|
+
questions: QuestionInfo[];
|
|
11
|
+
tool?: { messageID: string; callID: string };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function openCodeQuestionKey(
|
|
15
|
+
nativeRequest: OpenCodeQuestionRequest,
|
|
16
|
+
): string {
|
|
17
|
+
return JSON.stringify(nativeRequest.questions);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function toHarnessQuestionsInput(
|
|
21
|
+
nativeRequest: OpenCodeQuestionRequest,
|
|
22
|
+
): HarnessV1QuestionsToolInput {
|
|
23
|
+
return {
|
|
24
|
+
allowPartialAnswers: true,
|
|
25
|
+
questions: nativeRequest.questions.map((question, questionIndex) => ({
|
|
26
|
+
id: `question-${questionIndex + 1}`,
|
|
27
|
+
question: question.question,
|
|
28
|
+
header: question.header,
|
|
29
|
+
options: question.options.map((option, optionIndex) => ({
|
|
30
|
+
id: `option-${optionIndex + 1}`,
|
|
31
|
+
label: option.label,
|
|
32
|
+
description: option.description,
|
|
33
|
+
})),
|
|
34
|
+
...(question.multiple !== undefined
|
|
35
|
+
? { allowMultiple: question.multiple }
|
|
36
|
+
: {}),
|
|
37
|
+
...(question.custom !== undefined
|
|
38
|
+
? { allowFreeForm: question.custom }
|
|
39
|
+
: {}),
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function toOpenCodeQuestionResponse(input: {
|
|
45
|
+
nativeRequest: OpenCodeQuestionRequest;
|
|
46
|
+
output: HarnessV1QuestionsToolOutput;
|
|
47
|
+
}): { action: 'reply'; answers: string[][] } | { action: 'reject' } {
|
|
48
|
+
if (
|
|
49
|
+
input.output.action === 'declined' ||
|
|
50
|
+
input.output.action === 'cancelled'
|
|
51
|
+
) {
|
|
52
|
+
return { action: 'reject' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const answers = input.nativeRequest.questions.map(
|
|
56
|
+
(nativeQuestion, questionIndex) => {
|
|
57
|
+
const answer =
|
|
58
|
+
input.output.action === 'answered' ||
|
|
59
|
+
input.output.action === 'partially-answered'
|
|
60
|
+
? input.output.answers[`question-${questionIndex + 1}`]
|
|
61
|
+
: undefined;
|
|
62
|
+
if (answer == null) return [];
|
|
63
|
+
const selectedLabels = answer.optionIds.flatMap(optionId => {
|
|
64
|
+
const index = positionalIdIndex({
|
|
65
|
+
id: optionId,
|
|
66
|
+
prefix: 'option-',
|
|
67
|
+
});
|
|
68
|
+
const option =
|
|
69
|
+
index == null ? undefined : nativeQuestion.options[index];
|
|
70
|
+
return option == null ? [] : [option.label];
|
|
71
|
+
});
|
|
72
|
+
return answer.freeform === undefined
|
|
73
|
+
? selectedLabels
|
|
74
|
+
: [...selectedLabels, answer.freeform];
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return { action: 'reply', answers };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function positionalIdIndex(input: {
|
|
82
|
+
id: string;
|
|
83
|
+
prefix: string;
|
|
84
|
+
}): number | undefined {
|
|
85
|
+
if (!input.id.startsWith(input.prefix)) return undefined;
|
|
86
|
+
const oneBasedIndex = Number(input.id.slice(input.prefix.length));
|
|
87
|
+
return Number.isInteger(oneBasedIndex) && oneBasedIndex > 0
|
|
88
|
+
? oneBasedIndex - 1
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
package/src/opencode-harness.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import {
|
|
3
3
|
commonTool,
|
|
4
|
+
HARNESS_V1_BUILTIN_TOOLS,
|
|
4
5
|
HarnessCapabilityUnsupportedError,
|
|
5
6
|
harnessV1DiagnosticFromBridgeFrame,
|
|
6
7
|
type HarnessV1,
|
|
@@ -123,6 +124,11 @@ export type OpenCodeHarnessSettings = {
|
|
|
123
124
|
const optionalStringRecord = z.record(z.string(), z.unknown()).optional();
|
|
124
125
|
|
|
125
126
|
const OPENCODE_BUILTIN_TOOLS = {
|
|
127
|
+
askUserQuestions: {
|
|
128
|
+
...HARNESS_V1_BUILTIN_TOOLS.askUserQuestions,
|
|
129
|
+
nativeName: 'question',
|
|
130
|
+
toolUseKind: 'readonly',
|
|
131
|
+
},
|
|
126
132
|
read: commonTool('read', {
|
|
127
133
|
nativeName: 'view',
|
|
128
134
|
toolUseKind: 'readonly',
|
|
@@ -336,8 +342,6 @@ export function createOpenCode(
|
|
|
336
342
|
);
|
|
337
343
|
}
|
|
338
344
|
credentialsBrokered = true;
|
|
339
|
-
} else {
|
|
340
|
-
warnCredentialBrokeringUnavailable();
|
|
341
345
|
}
|
|
342
346
|
const bootstrapDir = path.posix.resolve(
|
|
343
347
|
defaultWorkingDirectory,
|
|
@@ -457,6 +461,14 @@ export function createOpenCode(
|
|
|
457
461
|
OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
|
|
458
462
|
credentialForwarding: settings.credentialForwarding,
|
|
459
463
|
});
|
|
464
|
+
if (!credentialsBrokered) {
|
|
465
|
+
warnCredentialBrokeringUnavailable({
|
|
466
|
+
environment: resolvedAuthEnvironment,
|
|
467
|
+
forwardedEnvironment: forwardedAuthEnvironment,
|
|
468
|
+
credentialEnvironmentVariables:
|
|
469
|
+
OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
460
472
|
const env = {
|
|
461
473
|
...forwardedAuthEnvironment,
|
|
462
474
|
AI_SDK_HARNESS_CLIENT_APP: OPENCODE_CLIENT_APP,
|
|
@@ -948,6 +960,9 @@ function createSession({
|
|
|
948
960
|
toolCallId: input.toolCallId,
|
|
949
961
|
output: input.output,
|
|
950
962
|
isError: input.isError,
|
|
963
|
+
...(input.toolResult !== undefined
|
|
964
|
+
? { toolResult: input.toolResult }
|
|
965
|
+
: {}),
|
|
951
966
|
});
|
|
952
967
|
},
|
|
953
968
|
submitToolApproval: async input => {
|