@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.
@@ -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: 'deny',
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 === 'permission.v2.asked') {
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
+ }
@@ -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 => {